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

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


The following commit(s) were added to refs/heads/main by this push:
     new 17653b39b1 [fixes #5300] Extend AWS SecretsManager test with rotating 
secret
17653b39b1 is described below

commit 17653b39b1dad5516eb147e6aa2788f5ed2a9a39
Author: Lukas Lowinger <[email protected]>
AuthorDate: Mon Dec 15 08:17:03 2025 +0100

    [fixes #5300] Extend AWS SecretsManager test with rotating secret
---
 .../aws2/aws-secrets-manager/README.adoc           |  28 ++++
 .../aws2/aws-secrets-manager/pom.xml               |  17 +++
 .../manager/it/AwsSecretsManagerResource.java      |  32 +++--
 .../secrets/manager/it/AwsSecretsManagerTest.java  | 145 +++++++++++++++++--
 .../src/test/resources/lambda/rotation_handler.py  | 155 +++++++++++++++++++++
 .../src/test/resources/lambda/rotation_handler.zip | Bin 0 -> 2311 bytes
 .../test/support/aws2/Aws2TestResource.java        |   2 +
 7 files changed, 357 insertions(+), 22 deletions(-)

diff --git a/integration-test-groups/aws2/aws-secrets-manager/README.adoc 
b/integration-test-groups/aws2/aws-secrets-manager/README.adoc
new file mode 100644
index 0000000000..2155d4f614
--- /dev/null
+++ b/integration-test-groups/aws2/aws-secrets-manager/README.adoc
@@ -0,0 +1,28 @@
+= AWS Secrets Manager tests
+
+== Rotating secret on real account
+
+To rotate secret on real account Lambda function has to be created at first.
+The code which will create it for you in 
`AwsSecretsManagerTest#setupLambdaFunction` expects following:
+
+* Created role (IAM -> Roles) with name `cq-lambda-role`.
+* The role has to have atleast `AWSLambdaBasicExecutionRole` and custom policy 
with:
+```
+                       "Sid": "<unique-name>",
+                       "Effect": "Allow",
+                       "Action": [
+                               "secretsmanager:DescribeSecret",
+                               "secretsmanager:GetSecretValue",
+                               "secretsmanager:PutSecretValue",
+                               "secretsmanager:UpdateSecretVersionStage"
+                       ],
+                       "Resource": 
"arn:aws:secretsmanager:<region>:<account-id>:secret:*"
+               }
+```
+where
+
+* `Sid` can be anything (eg. `CQSecretOperations`)
+* `region` the same which you export with `AWS_REGION` env
+* `account-id` can be found in the upper right corner in AWS Web UI
+
+
diff --git a/integration-test-groups/aws2/aws-secrets-manager/pom.xml 
b/integration-test-groups/aws2/aws-secrets-manager/pom.xml
index cef28462b8..c91612b399 100644
--- a/integration-test-groups/aws2/aws-secrets-manager/pom.xml
+++ b/integration-test-groups/aws2/aws-secrets-manager/pom.xml
@@ -43,6 +43,10 @@
             <groupId>org.apache.camel.quarkus</groupId>
             <artifactId>camel-quarkus-direct</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.camel.quarkus</groupId>
+            <artifactId>camel-quarkus-aws2-lambda</artifactId>
+        </dependency>
         <dependency>
             <groupId>io.quarkus</groupId>
             <artifactId>quarkus-resteasy</artifactId>
@@ -130,6 +134,19 @@
                         </exclusion>
                     </exclusions>
                 </dependency>
+                <dependency>
+                    <groupId>org.apache.camel.quarkus</groupId>
+                    
<artifactId>camel-quarkus-aws2-lambda-deployment</artifactId>
+                    <version>${project.version}</version>
+                    <type>pom</type>
+                    <scope>test</scope>
+                    <exclusions>
+                        <exclusion>
+                            <groupId>*</groupId>
+                            <artifactId>*</artifactId>
+                        </exclusion>
+                    </exclusions>
+                </dependency>
                 <dependency>
                     <groupId>org.apache.camel.quarkus</groupId>
                     <artifactId>camel-quarkus-aws2-sqs-deployment</artifactId>
diff --git 
a/integration-test-groups/aws2/aws-secrets-manager/src/main/java/org/apache/camel/quarkus/component/aws/secrets/manager/it/AwsSecretsManagerResource.java
 
b/integration-test-groups/aws2/aws-secrets-manager/src/main/java/org/apache/camel/quarkus/component/aws/secrets/manager/it/AwsSecretsManagerResource.java
index 0bcc15a23f..5811f68b52 100644
--- 
a/integration-test-groups/aws2/aws-secrets-manager/src/main/java/org/apache/camel/quarkus/component/aws/secrets/manager/it/AwsSecretsManagerResource.java
+++ 
b/integration-test-groups/aws2/aws-secrets-manager/src/main/java/org/apache/camel/quarkus/component/aws/secrets/manager/it/AwsSecretsManagerResource.java
@@ -44,7 +44,6 @@ import 
org.apache.camel.quarkus.test.support.aws2.BaseAws2Resource;
 import org.apache.camel.spi.PeriodTaskResolver;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.util.CollectionHelper;
-import org.jboss.logging.Logger;
 import 
software.amazon.awssdk.services.secretsmanager.model.CreateSecretResponse;
 import 
software.amazon.awssdk.services.secretsmanager.model.DeleteSecretRequest;
 import 
software.amazon.awssdk.services.secretsmanager.model.DeleteSecretResponse;
@@ -52,7 +51,9 @@ import 
software.amazon.awssdk.services.secretsmanager.model.DescribeSecretRespon
 import 
software.amazon.awssdk.services.secretsmanager.model.ListSecretsResponse;
 import 
software.amazon.awssdk.services.secretsmanager.model.ReplicateSecretToRegionsResponse;
 import 
software.amazon.awssdk.services.secretsmanager.model.RestoreSecretResponse;
+import 
software.amazon.awssdk.services.secretsmanager.model.RotateSecretRequest;
 import 
software.amazon.awssdk.services.secretsmanager.model.RotateSecretResponse;
+import software.amazon.awssdk.services.secretsmanager.model.RotationRulesType;
 import software.amazon.awssdk.services.secretsmanager.model.SecretListEntry;
 import 
software.amazon.awssdk.services.secretsmanager.model.UpdateSecretResponse;
 
@@ -60,13 +61,10 @@ import 
software.amazon.awssdk.services.secretsmanager.model.UpdateSecretResponse
 @ApplicationScoped
 public class AwsSecretsManagerResource extends BaseAws2Resource {
 
-    private static final Logger LOG = 
Logger.getLogger(AwsSecretsManagerResource.class);
-
-    private static final String COMPONENT_AWS_SECRETS_MANAGER = 
"aws-secrets-manager";
+    static final AtomicBoolean contextReloaded = new AtomicBoolean(false);
 
     @Inject
     CamelContext context;
-
     @Inject
     ProducerTemplate producerTemplate;
 
@@ -74,8 +72,6 @@ public class AwsSecretsManagerResource extends 
BaseAws2Resource {
         super(null, "camel.component.aws-secrets-manager");
     }
 
-    static final AtomicBoolean contextReloaded = new AtomicBoolean(false);
-
     void onReload(@Observes CamelContextReloadedEvent event) {
         contextReloaded.set(true);
     }
@@ -91,21 +87,36 @@ public class AwsSecretsManagerResource extends 
BaseAws2Resource {
             throws Exception {
 
         final Object resultBody;
+        boolean isPojoRequest = false;
         if (operation.equals("forceDeleteSecret")) {
             operation = SecretsManagerOperations.deleteSecret.toString();
             DeleteSecretRequest.Builder builder = 
DeleteSecretRequest.builder();
             builder.secretId((String) 
headers.get(SecretsManagerConstants.SECRET_ID));
             builder.forceDeleteWithoutRecovery(true);
             resultBody = builder.build();
+            isPojoRequest = true;
+        } else if (operation.equals("rotateSecretWithRotationRulesSet")) {
+            operation = SecretsManagerOperations.rotateSecret.toString();
+            RotateSecretRequest.Builder builder = 
RotateSecretRequest.builder();
+            builder.secretId((String) 
headers.get(SecretsManagerConstants.SECRET_ID));
+            builder.rotationLambdaARN((String) 
headers.get(SecretsManagerConstants.LAMBDA_ROTATION_FUNCTION_ARN));
+            builder.rotationRules(RotationRulesType.builder().build());
+            resultBody = builder.build();
+            isPojoRequest = true;
         } else {
             resultBody = body;
         }
 
         String region = headers.containsKey("region") ? 
String.format("region=%s&", headers.get("region")) : "";
 
-        String url = useHeaders ? 
String.format("aws-secrets-manager://test?%suseDefaultCredentialsProvider=%s",
-                region, isUseDefaultCredentials())
-                : 
String.format("aws-secrets-manager://test?%soperation=%s&useDefaultCredentialsProvider=%s",
+        String url = useHeaders
+                ? String.format(
+                        
"aws-secrets-manager://test?%suseDefaultCredentialsProvider=%s"
+                                + (isPojoRequest ? "&pojoRequest=true" : ""),
+                        region, isUseDefaultCredentials())
+                : String.format(
+                        
"aws-secrets-manager://test?%soperation=%s&useDefaultCredentialsProvider=%s"
+                                + (isPojoRequest ? "&pojoRequest=true" : ""),
                         region, operation, isUseDefaultCredentials());
 
         Exchange ex = producerTemplate.send(url,
@@ -180,5 +191,4 @@ public class AwsSecretsManagerResource extends 
BaseAws2Resource {
 
         return Response.ok(new 
URI("https://camel.apache.org/";)).entity(ex.getIn().getBody()).build();
     }
-
 }
diff --git 
a/integration-test-groups/aws2/aws-secrets-manager/src/test/java/org/apache/camel/quarkus/component/aws/secrets/manager/it/AwsSecretsManagerTest.java
 
b/integration-test-groups/aws2/aws-secrets-manager/src/test/java/org/apache/camel/quarkus/component/aws/secrets/manager/it/AwsSecretsManagerTest.java
index 88cc9a8da5..dba1ac303b 100644
--- 
a/integration-test-groups/aws2/aws-secrets-manager/src/test/java/org/apache/camel/quarkus/component/aws/secrets/manager/it/AwsSecretsManagerTest.java
+++ 
b/integration-test-groups/aws2/aws-secrets-manager/src/test/java/org/apache/camel/quarkus/component/aws/secrets/manager/it/AwsSecretsManagerTest.java
@@ -16,7 +16,10 @@
  */
 package org.apache.camel.quarkus.component.aws.secrets.manager.it;
 
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.Collections;
+import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
@@ -24,34 +27,117 @@ import io.quarkus.test.common.QuarkusTestResource;
 import io.quarkus.test.junit.QuarkusTest;
 import io.restassured.RestAssured;
 import io.restassured.http.ContentType;
+import io.smallrye.common.os.OS;
 import org.apache.camel.component.aws.secretsmanager.SecretsManagerConstants;
 import org.apache.camel.component.aws.secretsmanager.SecretsManagerOperations;
 import org.apache.camel.quarkus.test.EnabledIf;
 import org.apache.camel.quarkus.test.mock.backend.MockBackendDisabled;
 import org.apache.camel.quarkus.test.mock.backend.MockBackendUtils;
+import org.apache.camel.quarkus.test.support.aws2.Aws2Client;
 import org.apache.camel.quarkus.test.support.aws2.Aws2TestResource;
 import org.apache.camel.quarkus.test.support.aws2.BaseAWs2TestSupport;
+import org.apache.camel.util.CollectionHelper;
+import org.apache.commons.lang3.RandomStringUtils;
 import org.awaitility.Awaitility;
+import org.jboss.logging.Logger;
 import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.localstack.LocalStackContainer;
+import software.amazon.awssdk.services.lambda.LambdaClient;
+import software.amazon.awssdk.services.lambda.model.AddPermissionRequest;
+import software.amazon.awssdk.services.lambda.model.CreateFunctionRequest;
+import software.amazon.awssdk.services.lambda.model.DeleteFunctionRequest;
+import software.amazon.awssdk.services.lambda.model.FunctionCode;
+import software.amazon.awssdk.services.lambda.model.FunctionConfiguration;
+import software.amazon.awssdk.services.lambda.model.GetFunctionRequest;
+import software.amazon.awssdk.services.sts.StsClient;
 
 import static org.hamcrest.CoreMatchers.is;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 @QuarkusTest
 @QuarkusTestResource(Aws2TestResource.class)
 public class AwsSecretsManagerTest extends BaseAWs2TestSupport {
 
+    public static final String name2ToCreate = "CQTestSecret2-operation-2-" + 
System.currentTimeMillis();
+    private static final Logger log = 
Logger.getLogger(AwsSecretsManagerTest.class);
+    private static String lambdaArn;
+    private static String lambdaName;
+
+    @Aws2Client(LocalStackContainer.Service.LAMBDA)
+    LambdaClient lambdaClient;
+    @Aws2Client(LocalStackContainer.Service.STS)
+    StsClient stsClient;
+
     public AwsSecretsManagerTest() {
         super("/aws-secrets-manager");
     }
 
+    public void setupLambdaFunction() throws IOException {
+        String lambdaHandler = "rotation_handler.lambda_handler";
+        String awsAccountId = "000000000000";
+        if (!MockBackendUtils.startMockBackend(false)) {
+            awsAccountId = stsClient.getCallerIdentity().account();
+        }
+        String lambdaRole = 
String.format("arn:aws:iam::%s:role/cq-lambda-role", awsAccountId);
+        log.info("AWS Lambda role: %s".formatted(lambdaRole));
+        lambdaName = "cq-secret-rotator-" + 
RandomStringUtils.secure().nextAlphanumeric(20).toLowerCase(Locale.ROOT);
+
+        try (InputStream lambdaZip = 
Thread.currentThread().getContextClassLoader()
+                .getResourceAsStream("lambda/rotation_handler.zip")) {
+            // Create Lambda Function used for rotation
+            CreateFunctionRequest createRequest = 
CreateFunctionRequest.builder()
+                    .functionName(lambdaName)
+                    .runtime("python3.9")
+                    .role(lambdaRole)
+                    .handler(lambdaHandler)
+                    .code(FunctionCode.builder()
+                            
.zipFile(software.amazon.awssdk.core.SdkBytes.fromByteArray(lambdaZip.readAllBytes()))
+                            .build())
+                    .build();
+
+            lambdaClient.createFunction(createRequest);
+
+            // Getting ARN of created Lambda Function
+            GetFunctionRequest getRequest = 
GetFunctionRequest.builder().functionName(lambdaName).build();
+            FunctionConfiguration functionConfig = 
lambdaClient.getFunction(getRequest).configuration();
+            lambdaArn = functionConfig.functionArn();
+            log.info("AWS Lambda Arn: %s".formatted(lambdaArn));
+
+            if (!MockBackendUtils.startMockBackend(false)) {
+                AddPermissionRequest permissionRequest = 
AddPermissionRequest.builder()
+                        .functionName(lambdaArn)
+                        .statementId("SecretsManagerInvokePermission")
+                        .action("lambda:InvokeFunction")
+                        .principal("secretsmanager.amazonaws.com")
+                        .sourceArn("arn:aws:secretsmanager:%s:%s:secret:%s*"
+                                .formatted(System.getenv("AWS_REGION"), 
awsAccountId, AwsSecretsManagerTest.name2ToCreate))
+                        .build();
+
+                lambdaClient.addPermission(permissionRequest);
+            }
+        }
+    }
+
+    public void cleanLambdaFunction() {
+        if (lambdaName != null) {
+            
lambdaClient.deleteFunction(DeleteFunctionRequest.builder().functionName(lambdaName).build());
+        }
+    }
+
     @Test
-    public void testOperations() {
+    public void testOperations() throws IOException {
+        if (canUseLambdaFunction()) {
+            setupLambdaFunction();
+        }
+
         final String secretToCreate = "loadFirst";
         final String secret2ToCreate = "changeit2";
         final String secretToUpdate = "loadSecond";
         final String nameToCreate = "CQTestSecret-operation-1-" + 
System.currentTimeMillis();
-        final String name2ToCreate = "CQTestSecret2-operation-2-" + 
System.currentTimeMillis();
         final String description2ToCreate = "description-" + name2ToCreate;
         String createdArn = null;
         String createdArn2 = null;
@@ -59,9 +145,9 @@ public class AwsSecretsManagerTest extends 
BaseAWs2TestSupport {
         try {
             // >> create secret 1
             createdArn = AwsSecretsManagerUtil.createSecret(nameToCreate, 
secretToCreate);
-            // >> create secret 2 (with description)
             assertNotNull(createdArn);
 
+            // >> create secret 2 (with description)
             createdArn2 = RestAssured.given()
                     .contentType(ContentType.JSON)
                     .body(Map.of(SecretsManagerConstants.OPERATION, 
SecretsManagerOperations.createSecret,
@@ -74,7 +160,7 @@ public class AwsSecretsManagerTest extends 
BaseAWs2TestSupport {
                     .statusCode(201)
                     .extract().asString();
 
-            assertNotNull(createdArn);
+            assertNotNull(createdArn2);
 
             // >> list both secrets
             final String finalCreatedArn = createdArn;
@@ -92,7 +178,7 @@ public class AwsSecretsManagerTest extends 
BaseAWs2TestSupport {
                             () -> {
                                 Map<String, Boolean> secrets = 
AwsSecretsManagerUtil.listSecrets(1);
                                 // contains both created secrets
-                                assertTrue(secrets.size() == 1);
+                                assertEquals(1, secrets.size());
                             });
 
             // >> get secret1 with version_id
@@ -121,6 +207,39 @@ public class AwsSecretsManagerTest extends 
BaseAWs2TestSupport {
             assertEquals(name2ToCreate, descriptionMap.get("name"));
             assertEquals(description2ToCreate, 
descriptionMap.get("description"));
 
+            if (canUseLambdaFunction()) {
+                String rotateOperation = 
SecretsManagerOperations.rotateSecret.toString();
+                if (MockBackendUtils.startMockBackend(false)) {
+                    // rotate secret2 with rotation rules set to fix issue 
with LocalStack
+                    // on real AWS use the default Camel rotateSecret 
operation without POJO involved
+                    // this workaround is only for older LocalStack - it is 
not needed in 4.12.0 (probably fixed via 
https://github.com/localstack/localstack/pull/12391/)
+                    rotateOperation = "rotateSecretWithRotationRulesSet";
+                }
+
+                // rotate secret2
+                RestAssured.given()
+                        .contentType(ContentType.JSON)
+                        
.body(CollectionHelper.mapOf(SecretsManagerConstants.SECRET_ID, createdArn2,
+                                
SecretsManagerConstants.LAMBDA_ROTATION_FUNCTION_ARN, lambdaArn))
+                        .post("/aws-secrets-manager/operation/" + 
rotateOperation)
+                        .then()
+                        .statusCode(201)
+                        .body(is("true"));
+
+                Awaitility.await().pollInterval(5, TimeUnit.SECONDS).atMost(1, 
TimeUnit.MINUTES).untilAsserted(
+                        () -> {
+                            var secret2RotatedMap = RestAssured.given()
+                                    .contentType(ContentType.JSON)
+                                    
.body(Collections.singletonMap(SecretsManagerConstants.SECRET_ID, 
finalCreatedArn2))
+                                    .post("/aws-secrets-manager/operation/" + 
SecretsManagerOperations.getSecret)
+                                    .then()
+                                    .statusCode(201)
+                                    .extract().as(Map.class);
+
+                            assertEquals(secret2ToCreate + "_Rotated", 
secret2RotatedMap.get("body"));
+                        });
+            }
+
             // >> delete secret 2
             RestAssured.given()
                     .contentType(ContentType.JSON)
@@ -144,10 +263,6 @@ public class AwsSecretsManagerTest extends 
BaseAWs2TestSupport {
                         }
                     });
 
-            // operation rotateSecret fails on local stack with 500 when 
upgraded to 2.2.0
-            // it needs lambda function ARN to work
-            // TODO:See https://github.com/apache/camel-quarkus/issues/5300
-
             // >> update value of the first secret
             AwsSecretsManagerUtil.updateSecret(createdArn, secretToUpdate);
 
@@ -201,6 +316,10 @@ public class AwsSecretsManagerTest extends 
BaseAWs2TestSupport {
             // also on localstack, if not the second run of operations would 
fail
             AwsSecretsManagerUtil.deleteSecretImmediately(createdArn);
             AwsSecretsManagerUtil.deleteSecretImmediately(createdArn2);
+
+            if (canUseLambdaFunction()) {
+                cleanLambdaFunction();
+            }
         }
     }
 
@@ -213,7 +332,6 @@ public class AwsSecretsManagerTest extends 
BaseAWs2TestSupport {
         try {
             createdArn = AwsSecretsManagerUtil.createSecret(nameToCreate, 
secretToCreate);
             assertNotNull(createdArn);
-
         } finally {
             // we must clean created secrets
             // also on localstack, if not the second run of operations would 
fail
@@ -260,4 +378,9 @@ public class AwsSecretsManagerTest extends 
BaseAWs2TestSupport {
             }
         }
     }
+
+    private boolean canUseLambdaFunction() {
+        // https://github.com/testcontainers/testcontainers-java/issues/11342
+        return OS.current() != OS.MAC || 
!MockBackendUtils.startMockBackend(false);
+    }
 }
diff --git 
a/integration-test-groups/aws2/aws-secrets-manager/src/test/resources/lambda/rotation_handler.py
 
b/integration-test-groups/aws2/aws-secrets-manager/src/test/resources/lambda/rotation_handler.py
new file mode 100644
index 0000000000..9f20ad380f
--- /dev/null
+++ 
b/integration-test-groups/aws2/aws-secrets-manager/src/test/resources/lambda/rotation_handler.py
@@ -0,0 +1,155 @@
+#
+# 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.
+#
+
+import json
+import boto3
+import logging
+import os
+from botocore.exceptions import ClientError
+
+# --- Configuration ---
+logger = logging.getLogger()
+logger.setLevel(logging.INFO)
+
+# Determine the LocalStack endpoint dynamically
+ENDPOINT_URL = None
+
+if os.environ.get('LOCALSTACK_HOSTNAME'):
+    # Default LocalStack port is 4566
+    ENDPOINT_URL = 
f"http://{os.environ.get('LOCALSTACK_HOSTNAME')}:{os.environ.get('EDGE_PORT', 
4566)}"
+
+# Initialize Secrets Manager client
+secrets_manager = boto3.client(
+    'secretsmanager',
+    region_name=os.environ.get('AWS_REGION', 'us-east-1'), # Default 
LocalStack region is 'us-east-1'
+    endpoint_url=ENDPOINT_URL
+)
+
+def lambda_handler(event, context):
+    """
+    Handles the four steps of the AWS Secrets Manager rotation cycle.
+    """
+    logger.info(f"Received rotation event: {event}")
+
+    arn = event['SecretId']
+    token = event['ClientRequestToken']
+    step = event['Step']
+
+    # 1. Get Secret Metadata (Used to verify current and pending versions)
+    metadata = secrets_manager.describe_secret(SecretId=arn)
+    
+    # Check if the secret is currently being rotated with the given token
+    if not token in metadata['VersionIdsToStages']:
+        raise ValueError(f"Secret version {token} has no stage for rotation.")
+
+    # Determine the current version ID
+    current_version_id = None
+    for version_id, stages in metadata['VersionIdsToStages'].items():
+        if 'AWSCURRENT' in stages:
+            if version_id == token:
+                # The secret is already CURRENT, rotation completed previously
+                logger.info(f"Secret {arn} is already CURRENT for token 
{token}. Skipping steps.")
+                return {}
+            current_version_id = version_id
+            break
+
+    # --- Execute Rotation Steps ---
+    if step == 'createSecret':
+        return create_secret(arn, token, current_version_id)
+    
+    elif step == 'setSecret':
+        return set_secret(arn, token)
+        
+    elif step == 'testSecret':
+        return test_secret(arn, token)
+
+    elif step == 'finishSecret':
+        return finish_secret(arn, token, current_version_id)
+
+    else:
+        raise ValueError(f"Invalid step parameter: {step}")
+
+# --- ROTATION LOGIC IMPLEMENTATION ---
+
+def create_secret(arn, token, current_version_id):
+    """
+    (Step 1) Creates a new version of the secret with the PENDING stage label.
+    """
+    logger.info("Executing createSecret step.")
+    
+    # 1. Get the current secret value
+    current_value = secrets_manager.get_secret_value(SecretId=arn, 
VersionId=current_version_id)['SecretString']
+    
+    # 2. Rotation Logic: Simple numeric increment
+    try:
+        new_value = str(int(current_value) + 1)
+    except ValueError:
+        new_value = current_value + "_Rotated"
+
+    # 3. Store the new secret value tagged with the PENDING token
+    try:
+        secrets_manager.put_secret_value(
+            SecretId=arn,
+            ClientRequestToken=token,
+            SecretString=new_value,
+            VersionStages=['PENDING']
+        )
+        logger.info(f"New PENDING secret created with value: {new_value}")
+    except ClientError as e:
+        if e.response['Error']['Code'] == 'ResourceExistsException':
+             # PENDING version already exists, which is acceptable if 
re-running
+             logger.warning("PENDING secret already exists, continuing.")
+             pass
+        else:
+            raise e
+
+    return {}
+
+def set_secret(arn, token):
+    """(Step 2) Updates the underlying service/database. (Skipped for 
simulation)"""
+    logger.info("Executing setSecret step. (Skipped for simple LocalStack 
test)")
+    return {}
+
+def test_secret(arn, token):
+    """(Step 3) Validates the new secret. (Skipped for simulation)"""
+    logger.info("Executing testSecret step. (Skipped for simple LocalStack 
test)")
+    return {}
+
+def finish_secret(arn, token, current_version_id):
+    """
+    (Step 4) Moves the AWSCURRENT label from the old version to the PENDING 
version.
+    Requires explicit removal of AWSCURRENT from the old ID for 
LocalStack/Moto.
+    """
+    logger.info("Executing finishSecret step.")
+
+    # Critical check based on the previous error logs
+    if not current_version_id:
+        logger.error("Current version ID is required but missing. Cannot 
finish rotation.")
+        # Raise exception to fail the rotation attempt
+        raise ValueError("Missing current_version_id for finishSecret.")
+
+    # Shift the AWSCURRENT label to the version identified by the token
+    secrets_manager.update_secret_version_stage(
+        SecretId=arn,
+        VersionStage='AWSCURRENT',
+        MoveToVersionId=token,
+
+        # KEY FIX: Explicitly remove AWSCURRENT from the old version ID
+        RemoveFromVersionId=current_version_id
+    )
+    logger.info(f"Rotation finished. AWSCURRENT moved from 
{current_version_id} to {token}.")
+    return {}
\ No newline at end of file
diff --git 
a/integration-test-groups/aws2/aws-secrets-manager/src/test/resources/lambda/rotation_handler.zip
 
b/integration-test-groups/aws2/aws-secrets-manager/src/test/resources/lambda/rotation_handler.zip
new file mode 100644
index 0000000000..aaaae0a6c0
Binary files /dev/null and 
b/integration-test-groups/aws2/aws-secrets-manager/src/test/resources/lambda/rotation_handler.zip
 differ
diff --git 
a/integration-tests-support/aws2/src/test/java/org/apache/camel/quarkus/test/support/aws2/Aws2TestResource.java
 
b/integration-tests-support/aws2/src/test/java/org/apache/camel/quarkus/test/support/aws2/Aws2TestResource.java
index b7d929a66d..79d004571b 100644
--- 
a/integration-tests-support/aws2/src/test/java/org/apache/camel/quarkus/test/support/aws2/Aws2TestResource.java
+++ 
b/integration-tests-support/aws2/src/test/java/org/apache/camel/quarkus/test/support/aws2/Aws2TestResource.java
@@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory;
 import org.testcontainers.containers.localstack.LocalStackContainer;
 import org.testcontainers.containers.localstack.LocalStackContainer.Service;
 import org.testcontainers.containers.output.Slf4jLogConsumer;
+import org.testcontainers.containers.wait.strategy.Wait;
 import org.testcontainers.utility.DockerImageName;
 import software.amazon.awssdk.core.SdkClient;
 
@@ -91,6 +92,7 @@ public final class Aws2TestResource implements 
QuarkusTestResourceLifecycleManag
             localstack.withEnv("PROVIDER_OVERRIDE_CLOUDWATCH", "v1");
             localstack.withEnv("AWS_ACCESS_KEY_ID", "testAccessKeyId"); //has 
to be longer then `test`, to work on FIPS systems
             localstack.withEnv("AWS_SECRET_ACCESS_KEY", "testSecretKeyId");
+            localstack.waitingFor(Wait.forListeningPort()); // it could happen 
the port is not ready yet in rapid local development
             localstack.withLogConsumer(new Slf4jLogConsumer(LOG));
             localstack.start();
 

Reply via email to