Repository: floci-io/floci (2026-04-11T12:26:19Z)

Issue:
[BUG] Kinesis: IncreaseStreamRetentionPeriod with same value should be a no-op (not an error)
## Service

Kinesis

## AWS API Action

`IncreaseStreamRetentionPeriod` — `POST /` with `Action=IncreaseStreamRetentionPeriod`

## Related issue

This is a follow-up to #291 (which added the `IncreaseStreamRetentionPeriod` implementation). The API now exists but its error handling differs from real AWS in an edge case that breaks Terraform.

## Expected behavior (real AWS)

On real AWS, calling `IncreaseStreamRetentionPeriod` with the **same value** as the current retention period is a **no-op** — it returns success without error.

```bash
# Stream already has 24h retention
aws kinesis increase-stream-retention-period \
  --stream-name my-stream \
  --retention-period-hours 24
# ✅ Returns success (no-op)
```

This is important because Terraform calls `IncreaseStreamRetentionPeriod` on every apply when the `retention_period` attribute is set, even if the value hasn't changed.

## Actual behavior (Floci 1.5.2)

Floci returns `InvalidArgumentException` when the requested retention period equals the current retention period:

```
InvalidArgumentException: Requested retention period (24 hours) must be 
greater than current retention period (24 hours)
```

This is technically correct per the API name ("increase"), but it differs from real AWS behavior and causes Terraform to **taint the Kinesis stream resource**. Tainted streams cause cascading failures — dependent resources like Lambda Event Source Mappings fail to create.

### Reproduction

```bash
# Start Floci
docker run -d -p 4566:4566 hectorvent/floci:latest

# Create a Kinesis stream (default retention: 24h)
aws --endpoint-url http://localhost:4566 kinesis create-stream \
  --stream-name test-stream \
  --shard-count 1

# Set retention to 24h (same as default) — this should be a no-op
aws --endpoint-url http://localhost:4566 kinesis increase-stream-retention-period \
  --stream-name test-stream \
  --retention-period-hours 24
# ❌ InvalidArgumentException: Requested retention period (24 hours) must be 
#    greater than current retention period (24 hours)
```

### Terraform context

This specifically affects repos using **nested Terraform modules** where the inner module has a hardcoded default of 24h that can't be overridden from the outer module:

```hcl
# Inner module (infra-terraform shared module)
variable "retention_period" {
  default = 24
}

resource "aws_kinesis_stream" "stream" {
  name             = var.stream_name
  shard_count      = var.shard_count
  retention_period = var.retention_period  # Always 24, can't be set to null
}
```

Terraform output:
```
Error: increasing Kinesis Stream (my-stream) retention period: 
InvalidArgumentException: Requested retention period (24 hours) must be 
greater than current retention period (24 hours)
```

## Impact

- Terraform taints the Kinesis stream resource
- Dependent resources (Lambda ESMs, consumers) fail to create
- `lifecycle { ignore_changes = [retention_period] }` only works when you control the module definition (not possible with shared/nested modules)
- `|| true` absorbs the error but leaves the stream tainted in Terraform state

## Suggested fix

If the requested retention period equals the current retention period, return success (HTTP 200, empty body) instead of `InvalidArgumentException`. This matches real AWS behavior.

## Environment

- Floci version: 1.5.2 (`hectorvent/floci:latest` as of April 2026)
- Terraform AWS provider: v5.x
- Running Floci in Docker Compose


Discussion:
Verified — IncreaseStreamRetentionPeriod works correctly in 1.5.3, both for no-op (same value) and actual increases (e.g. 24h to 48h). Workaround removed. Thank you!



Patch:
diff --git a/src/main/java/io/github/hectorvent/floci/services/kinesis/KinesisService.java b/src/main/java/io/github/hectorvent/floci/services/kinesis/KinesisService.java
index 00688b14..0833efe2 100644
--- a/src/main/java/io/github/hectorvent/floci/services/kinesis/KinesisService.java
+++ b/src/main/java/io/github/hectorvent/floci/services/kinesis/KinesisService.java
@@ -147,12 +147,19 @@ public void increaseStreamRetentionPeriod(String streamName, int retentionPeriod
             throw new AwsException("InvalidArgumentException",
                     "Retention period must not exceed 8760 hours (365 days)", 400);
         }
-        if (retentionPeriodHours <= stream.getRetentionPeriodHours()) {
+        if (retentionPeriodHours < stream.getRetentionPeriodHours()) {
             throw new AwsException("InvalidArgumentException",
                     "Requested retention period (" + retentionPeriodHours +
-                    " hours) must be greater than current retention period (" +
+                    " hours) must not be less than current retention period (" +
                     stream.getRetentionPeriodHours() + " hours)", 400);
         }
+        // Same value is a no-op on real AWS despite the API doc wording ("must be more than
+        // current"). Proof: terraform-provider-aws calls IncreaseStreamRetentionPeriod on
+        // stream creation unconditionally when retention_period is set (stream.go Create path),
+        // so every default-retention TF stream would fail if AWS rejected same-value. See #342.
+        if (retentionPeriodHours == stream.getRetentionPeriodHours()) {
+            return;
+        }
         stream.setRetentionPeriodHours(retentionPeriodHours);
         store.put(regionKey(region, streamName), stream);
         LOG.infov("Increased retention period for stream {0} to {1} hours", streamName, retentionPeriodHours);
@@ -164,12 +171,16 @@ public void decreaseStreamRetentionPeriod(String streamName, int retentionPeriod
             throw new AwsException("InvalidArgumentException",
                     "Retention period must not be less than 24 hours", 400);
         }
-        if (retentionPeriodHours >= stream.getRetentionPeriodHours()) {
+        if (retentionPeriodHours > stream.getRetentionPeriodHours()) {
             throw new AwsException("InvalidArgumentException",
                     "Requested retention period (" + retentionPeriodHours +
-                    " hours) must be less than current retention period (" +
+                    " hours) must not be greater than current retention period (" +
                     stream.getRetentionPeriodHours() + " hours)", 400);
         }
+        // Same value is a no-op on real AWS (mirrors IncreaseStreamRetentionPeriod). See #342.
+        if (retentionPeriodHours == stream.getRetentionPeriodHours()) {
+            return;
+        }
         stream.setRetentionPeriodHours(retentionPeriodHours);
         store.put(regionKey(region, streamName), stream);
         LOG.infov("Decreased retention period for stream {0} to {1} hours", streamName, retentionPeriodHours);


Test patch:
diff --git a/src/test/java/io/github/hectorvent/floci/services/kinesis/KinesisIntegrationTest.java b/src/test/java/io/github/hectorvent/floci/services/kinesis/KinesisIntegrationTest.java
index f5d3d275..6fe39eb8 100644
--- a/src/test/java/io/github/hectorvent/floci/services/kinesis/KinesisIntegrationTest.java
+++ b/src/test/java/io/github/hectorvent/floci/services/kinesis/KinesisIntegrationTest.java
@@ -396,6 +396,66 @@ void increaseRetentionPeriodRejectsLowerValue() {
 
     @Test
     @Order(12)
+    void increaseRetentionPeriodSameValueIsNoOp() {
+        // Stream is currently at 48 (from Order 11). Increase to 48 should be a no-op,
+        // not an InvalidArgumentException. See #342: real AWS accepts same-value
+        // (terraform-provider-aws stream.go Create path calls this unconditionally on
+        // stream creation with the configured retention_period, so every default-retention
+        // TF stream would fail on first apply if AWS rejected same-value).
+        given()
+            .header("X-Amz-Target", "Kinesis_20131202.IncreaseStreamRetentionPeriod")
+            .contentType(KINESIS_CONTENT_TYPE)
+            .body("""
+                {"StreamName": "retention-test", "RetentionPeriodHours": 48}
+                """)
+        .when()
+            .post("/")
+        .then()
+            .statusCode(200);
+
+        given()
+            .header("X-Amz-Target", "Kinesis_20131202.DescribeStream")
+            .contentType(KINESIS_CONTENT_TYPE)
+            .body("""
+                {"StreamName": "retention-test"}
+                """)
+        .when()
+            .post("/")
+        .then()
+            .statusCode(200)
+            .body("StreamDescription.RetentionPeriodHours", equalTo(48));
+    }
+
+    @Test
+    @Order(13)
+    void decreaseRetentionPeriodSameValueIsNoOp() {
+        // Stream is still at 48. Decrease to 48 should also be a no-op.
+        given()
+            .header("X-Amz-Target", "Kinesis_20131202.DecreaseStreamRetentionPeriod")
+            .contentType(KINESIS_CONTENT_TYPE)
+            .body("""
+                {"StreamName": "retention-test", "RetentionPeriodHours": 48}
+                """)
+        .when()
+            .post("/")
+        .then()
+            .statusCode(200);
+
+        given()
+            .header("X-Amz-Target", "Kinesis_20131202.DescribeStream")
+            .contentType(KINESIS_CONTENT_TYPE)
+            .body("""
+                {"StreamName": "retention-test"}
+                """)
+        .when()
+            .post("/")
+        .then()
+            .statusCode(200)
+            .body("StreamDescription.RetentionPeriodHours", equalTo(48));
+    }
+
+    @Test
+    @Order(14)
     void listShardsForNonExistentStream() {
         given()
             .header("X-Amz-Target", "Kinesis_20131202.ListShards")