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();