Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/configuration/advanced/application-yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ floci:

rds:
enabled: true
mock: false # true = clusters/instances created without Docker (useful for CI)
proxy-base-port: 7001
proxy-max-port: 7099
default-postgres-image: "postgres:16-alpine"
Expand Down
1 change: 1 addition & 0 deletions docs/configuration/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ These services spawn Docker containers. They require access to the Docker socket
| Variable | Default | Description |
|---|---|---|
| `FLOCI_SERVICES_RDS_ENABLED` | `true` | Enable the RDS service |
| `FLOCI_SERVICES_RDS_MOCK` | `false` | When `true`, DB clusters and instances are created instantly without a real container or auth proxy (API only) |
| `FLOCI_SERVICES_RDS_PROXY_BASE_PORT` | `7001` | First port in the RDS proxy range |
| `FLOCI_SERVICES_RDS_PROXY_MAX_PORT` | `7099` | Last port in the RDS proxy range |
| `FLOCI_SERVICES_RDS_DEFAULT_POSTGRES_IMAGE` | `postgres:16-alpine` | Default PostgreSQL Docker image |
Expand Down
22 changes: 22 additions & 0 deletions docs/services/rds.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ RDS Data API (`rds-data`) is documented separately because it uses REST JSON rou
| Variable | Default | Description |
|---|---|---|
| `FLOCI_SERVICES_RDS_ENABLED` | `true` | Enable or disable the service |
| `FLOCI_SERVICES_RDS_MOCK` | `false` | `true` = metadata only (no Docker container or auth proxy) |
| `FLOCI_SERVICES_RDS_PROXY_BASE_PORT` | `7000` | First host port in the RDS proxy range |
| `FLOCI_SERVICES_RDS_PROXY_MAX_PORT` | `7099` | Last host port in the RDS proxy range |
| `FLOCI_SERVICES_RDS_DEFAULT_POSTGRES_IMAGE` | `postgres:16-alpine` | Docker image for PostgreSQL instances |
Expand All @@ -64,6 +65,27 @@ services:
FLOCI_SERVICES_RDS_PROXY_BASE_PORT: "7001"
```

### Mock mode (CI / tests)

Set `FLOCI_SERVICES_RDS_MOCK=true` when you only need the management API shape: clusters and
instances are registered as `available` immediately, with no Docker container or auth proxy behind
them. Each resource still gets a unique endpoint port, but nothing listens on it.

```yaml
# docker-compose.yml — CI / test environment
services:
floci:
image: floci/floci:latest
environment:
FLOCI_SERVICES_RDS_MOCK: "true"
```

!!! note "Switching modes over persisted state"
With a persistent storage mode, changing `FLOCI_SERVICES_RDS_MOCK` between restarts is
best-effort, as with the other mock-capable services: resources created in real mode and
deleted under mock leave their containers and volumes behind, and resources created in mock
mode are restored with fresh, empty containers when loaded in real mode.

## Examples

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,12 @@ interface RdsServiceConfig {
@WithDefault("true")
boolean enabled();

/** When true, DB clusters and instances are created instantly without a real Docker
* container or auth proxy (API/metadata only). Useful for CI and environments without
* access to the Docker socket. */
@WithDefault("false")
boolean mock();

@WithDefault("7000")
int proxyBasePort();

Expand Down
180 changes: 112 additions & 68 deletions src/main/java/io/github/hectorvent/floci/services/rds/RdsService.java

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ floci:
default-image: "valkey/valkey:8"
rds:
enabled: true
mock: false # FLOCI_SERVICES_RDS_MOCK — true = metadata only, no Docker (useful for CI)
proxy-base-port: 7001
proxy-max-port: 7099
default-postgres-image: "postgres:16-alpine"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.github.hectorvent.floci.services.rds.container.RdsContainerHandle;
import io.github.hectorvent.floci.services.rds.container.RdsContainerManager;
import io.github.hectorvent.floci.services.rds.model.DbInstance;
import io.github.hectorvent.floci.services.rds.model.DbInstanceStatus;
import io.github.hectorvent.floci.services.rds.model.DbParameterGroup;
import io.github.hectorvent.floci.services.rds.model.DbSubnetGroup;
import io.github.hectorvent.floci.services.rds.proxy.RdsProxyManager;
Expand Down Expand Up @@ -341,6 +342,108 @@ void deleteDbClusterFailsWhenMembersRemain() {
assertTrue(exception.getMessage().contains("still has DB instances"));
}

@Test
void mockModeCreatesClusterAvailableWithoutContainerOrProxy() {
when(config.services().rds().mock()).thenReturn(true);

DbCluster cluster = rdsService.createDbCluster("cluster1", "aurora-postgresql", "16.3",
"admin", "password", "dbname", false, null);

assertEquals(DbInstanceStatus.AVAILABLE, cluster.getStatus());
assertEquals("localhost", cluster.getEndpoint().address());
assertTrue(cluster.getEndpoint().port() > 0);
assertNull(cluster.getContainerId());
verify(containerManager, never()).start(any(), any(), any(), any(), any(), any(), any());
verify(proxyManager, never()).startProxy(any(), any(), anyBoolean(), anyInt(), any(), anyInt(),
any(), any(), any(), any());
}

@Test
void mockModeCreatesClusterInstanceAvailableWithoutContainer() {
when(config.services().rds().mock()).thenReturn(true);
rdsService.createDbCluster("cluster1", "aurora-postgresql", "16.3",
"admin", "password", "dbname", false, null);

DbInstance instance = rdsService.createDbInstance("inst1", "aurora-postgresql", "16.3",
"admin", "password", "dbname", "db.serverless",
0, false, null, null, "cluster1");

assertEquals(DbInstanceStatus.AVAILABLE, instance.getStatus());
assertEquals("localhost", instance.getEndpoint().address());
// No Docker volume name may be persisted: the mock cluster has a null volume id, so the
// fallback would fabricate a name that a later non-mock restore could try to reference.
assertNull(instance.getDockerVolumeName());
verify(containerManager, never()).start(any(), any(), any(), any(), any(), any(), any());
verify(proxyManager, never()).startProxy(any(), any(), anyBoolean(), anyInt(), any(), anyInt(),
any(), any(), any(), any());
}

@Test
void mockModeCreatesStandaloneInstanceAvailableWithoutContainer() {
when(config.services().rds().mock()).thenReturn(true);

DbInstance instance = rdsService.createDbInstance("standalone", "postgres", "16",
"admin", "password", "dbname", "db.t3.micro",
20, false, null, null, null);

assertEquals(DbInstanceStatus.AVAILABLE, instance.getStatus());
assertNull(instance.getContainerId());
verify(containerManager, never()).start(any(), any(), any(), any(), any(), any(), any());
}

@Test
void mockModeDeleteClusterSkipsDockerCleanup() {
when(config.services().rds().mock()).thenReturn(true);
rdsService.createDbCluster("cluster1", "aurora-postgresql", "16.3",
Comment thread
greptile-apps[bot] marked this conversation as resolved.
"admin", "password", "dbname", false, null);

rdsService.deleteDbCluster("cluster1");

verify(containerManager, never()).stop(any());
verify(containerManager, never()).removeVolume(any(), any());
}

@Test
void mockModeDeleteStandaloneInstanceSkipsDockerCleanup() {
when(config.services().rds().mock()).thenReturn(true);
rdsService.createDbInstance("standalone", "postgres", "16",
"admin", "password", "dbname", "db.t3.micro",
20, false, null, null, null);

rdsService.deleteDbInstance("standalone");

verify(containerManager, never()).stop(any());
verify(containerManager, never()).removeVolume(any(), any());
}

@Test
void mockModeAssignsDistinctEndpointPorts() {
when(config.services().rds().mock()).thenReturn(true);

DbCluster a = rdsService.createDbCluster("cluster-a", "aurora-postgresql", "16.3",
"admin", "password", "dbname", false, null);
DbCluster b = rdsService.createDbCluster("cluster-b", "aurora-postgresql", "16.3",
"admin", "password", "dbname", false, null);

assertNotEquals(a.getEndpoint().port(), b.getEndpoint().port());
}

@Test
void mockModeRebootSkipsContainerAndProxy() {
when(config.services().rds().mock()).thenReturn(true);
rdsService.createDbInstance("standalone", "postgres", "16",
"admin", "password", "dbname", "db.t3.micro",
20, false, null, null, null);

DbInstance rebooted = rdsService.rebootDbInstance("standalone");

assertEquals(DbInstanceStatus.AVAILABLE, rebooted.getStatus());
verify(containerManager, never()).start(any(), any(), any(), any(), any(), any(), any());
verify(containerManager, never()).stop(any());
verify(proxyManager, never()).startProxy(any(), any(), anyBoolean(), anyInt(), any(), anyInt(),
any(), any(), any(), any());
}

@Test
void createDbClusterRejectsUnknownClusterParameterGroup() {
AwsException exception = assertThrows(AwsException.class, () -> rdsService.createDbCluster("cluster1", "aurora-postgresql", "16.3",
Expand Down
1 change: 1 addition & 0 deletions src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ floci:
default-image: "valkey/valkey:8"
rds:
enabled: true
mock: false
proxy-base-port: 7001
proxy-max-port: 7099
default-postgres-image: "postgres:16-alpine"
Expand Down