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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.github.hectorvent.floci.core.common.AwsQueryResponse;
import io.github.hectorvent.floci.core.common.XmlBuilder;
import io.github.hectorvent.floci.services.rds.model.DbCluster;
import io.github.hectorvent.floci.services.rds.model.DbClusterParameterGroup;
import io.github.hectorvent.floci.services.rds.model.DbEndpoint;
import io.github.hectorvent.floci.services.rds.model.DbInstance;
import io.github.hectorvent.floci.services.rds.model.DbInstanceStatus;
Expand Down Expand Up @@ -55,6 +56,11 @@ public Response handle(String action, MultivaluedMap<String, String> params) {
case "DeleteDBParameterGroup" -> handleDeleteDbParameterGroup(params);
case "ModifyDBParameterGroup" -> handleModifyDbParameterGroup(params);
case "DescribeDBParameters" -> handleDescribeDbParameters(params);
case "CreateDBClusterParameterGroup" -> handleCreateDbClusterParameterGroup(params);
case "DescribeDBClusterParameterGroups" -> handleDescribeDbClusterParameterGroups(params);
case "DeleteDBClusterParameterGroup" -> handleDeleteDbClusterParameterGroup(params);
case "ModifyDBClusterParameterGroup" -> handleModifyDbClusterParameterGroup(params);
case "DescribeDBClusterParameters" -> handleDescribeDbClusterParameters(params);
default -> AwsQueryResponse.error("UnsupportedOperation",
"Operation " + action + " is not supported.", AwsNamespaces.RDS, 400);
};
Expand Down Expand Up @@ -344,6 +350,101 @@ private Response handleDescribeDbParameters(MultivaluedMap<String, String> param
}
}

// ── Cluster Parameter Groups ──────────────────────────────────────────────

private Response handleCreateDbClusterParameterGroup(MultivaluedMap<String, String> params) {
String name = params.getFirst("DBClusterParameterGroupName");
String family = params.getFirst("DBParameterGroupFamily");
String description = params.getFirst("Description");
if (name == null || name.isBlank()) {
return AwsQueryResponse.error("InvalidParameterValue", "DBClusterParameterGroupName is required.", AwsNamespaces.RDS, 400);
}
try {
DbClusterParameterGroup group = service.createDbClusterParameterGroup(name, family, description);
String result = clusterParamGroupXml(group);
return Response.ok(AwsQueryResponse.envelope("CreateDBClusterParameterGroup", AwsNamespaces.RDS, result)).build();
} catch (AwsException e) {
return AwsQueryResponse.error(e.getErrorCode(), e.getMessage(), AwsNamespaces.RDS, e.getHttpStatus());
}
}

private Response handleDescribeDbClusterParameterGroups(MultivaluedMap<String, String> params) {
String filterName = params.getFirst("DBClusterParameterGroupName");
try {
Collection<DbClusterParameterGroup> result = service.listDbClusterParameterGroups(filterName);
XmlBuilder xml = new XmlBuilder().start("DBClusterParameterGroups");
for (DbClusterParameterGroup g : result) {
xml.start("DBClusterParameterGroup").raw(clusterParamGroupInnerXml(g)).end("DBClusterParameterGroup");
}
xml.end("DBClusterParameterGroups").start("Marker").end("Marker");
return Response.ok(AwsQueryResponse.envelope("DescribeDBClusterParameterGroups", AwsNamespaces.RDS, xml.build())).build();
} catch (AwsException e) {
return AwsQueryResponse.error(e.getErrorCode(), e.getMessage(), AwsNamespaces.RDS, e.getHttpStatus());
}
}

private Response handleDeleteDbClusterParameterGroup(MultivaluedMap<String, String> params) {
String name = params.getFirst("DBClusterParameterGroupName");
if (name == null || name.isBlank()) {
return AwsQueryResponse.error("InvalidParameterValue", "DBClusterParameterGroupName is required.", AwsNamespaces.RDS, 400);
}
try {
service.deleteDbClusterParameterGroup(name);
return Response.ok(AwsQueryResponse.envelopeNoResult("DeleteDBClusterParameterGroup", AwsNamespaces.RDS)).build();
} catch (AwsException e) {
return AwsQueryResponse.error(e.getErrorCode(), e.getMessage(), AwsNamespaces.RDS, e.getHttpStatus());
}
}

private Response handleModifyDbClusterParameterGroup(MultivaluedMap<String, String> params) {
String name = params.getFirst("DBClusterParameterGroupName");
if (name == null || name.isBlank()) {
return AwsQueryResponse.error("InvalidParameterValue", "DBClusterParameterGroupName is required.", AwsNamespaces.RDS, 400);
}
Map<String, String> parameters = new HashMap<>();
for (int n = 1; ; n++) {
String paramName = params.getFirst("Parameters.member." + n + ".ParameterName");
if (paramName == null) {
break;
}
String paramValue = params.getFirst("Parameters.member." + n + ".ParameterValue");
if (paramValue != null) {
parameters.put(paramName, paramValue);
}
}
try {
DbClusterParameterGroup group = service.modifyDbClusterParameterGroup(name, parameters);
String result = new XmlBuilder()
.elem("DBClusterParameterGroupName", group.getDbClusterParameterGroupName())
.build();
return Response.ok(AwsQueryResponse.envelope("ModifyDBClusterParameterGroup", AwsNamespaces.RDS, result)).build();
} catch (AwsException e) {
return AwsQueryResponse.error(e.getErrorCode(), e.getMessage(), AwsNamespaces.RDS, e.getHttpStatus());
}
}

private Response handleDescribeDbClusterParameters(MultivaluedMap<String, String> params) {
String name = params.getFirst("DBClusterParameterGroupName");
if (name == null || name.isBlank()) {
return AwsQueryResponse.error("InvalidParameterValue", "DBClusterParameterGroupName is required.", AwsNamespaces.RDS, 400);
}
try {
DbClusterParameterGroup group = service.getDbClusterParameterGroup(name);
XmlBuilder xml = new XmlBuilder().start("Parameters");
for (Map.Entry<String, String> entry : group.getParameters().entrySet()) {
xml.start("member")
.elem("ParameterName", entry.getKey())
.elem("ParameterValue", entry.getValue())
.elem("IsModifiable", true)
.end("member");
}
xml.end("Parameters").start("Marker").end("Marker");
return Response.ok(AwsQueryResponse.envelope("DescribeDBClusterParameters", AwsNamespaces.RDS, xml.build())).build();
} catch (AwsException e) {
return AwsQueryResponse.error(e.getErrorCode(), e.getMessage(), AwsNamespaces.RDS, e.getHttpStatus());
}
}

// ── XML builders ──────────────────────────────────────────────────────────

private String dbInstanceXml(DbInstance i) {
Expand Down Expand Up @@ -472,6 +573,18 @@ private String paramGroupInnerXml(DbParameterGroup g) {
.build();
}

private String clusterParamGroupXml(DbClusterParameterGroup g) {
return new XmlBuilder().start("DBClusterParameterGroup").raw(clusterParamGroupInnerXml(g)).end("DBClusterParameterGroup").build();
}

private String clusterParamGroupInnerXml(DbClusterParameterGroup g) {
return new XmlBuilder()
.elem("DBClusterParameterGroupName", g.getDbClusterParameterGroupName())
.elem("DBParameterGroupFamily", g.getDbParameterGroupFamily())
.elem("Description", g.getDescription())
.build();
}

private String statusLabel(DbInstanceStatus status) {
return switch (status) {
case CREATING -> "creating";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.github.hectorvent.floci.services.rds.container.RdsContainerManager;
import io.github.hectorvent.floci.services.rds.model.DatabaseEngine;
import io.github.hectorvent.floci.services.rds.model.DbCluster;
import io.github.hectorvent.floci.services.rds.model.DbClusterParameterGroup;
import io.github.hectorvent.floci.services.rds.model.DbEndpoint;
import io.github.hectorvent.floci.services.rds.model.DbInstance;
import io.github.hectorvent.floci.services.rds.model.DbInstanceStatus;
Expand Down Expand Up @@ -38,6 +39,7 @@ public class RdsService {
private final StorageBackend<String, DbInstance> instances;
private final StorageBackend<String, DbCluster> clusters;
private final StorageBackend<String, DbParameterGroup> parameterGroups;
private final StorageBackend<String, DbClusterParameterGroup> clusterParameterGroups;
private final RdsContainerManager containerManager;
private final RdsProxyManager proxyManager;
private final RegionResolver regionResolver;
Expand All @@ -56,6 +58,7 @@ public RdsService(RdsContainerManager containerManager,
this.instances = new InMemoryStorage<>();
this.clusters = new InMemoryStorage<>();
this.parameterGroups = new InMemoryStorage<>();
this.clusterParameterGroups = new InMemoryStorage<>();
}

// ── DB Instances ──────────────────────────────────────────────────────────
Expand Down Expand Up @@ -369,6 +372,49 @@ public DbParameterGroup modifyDbParameterGroup(String name,
return group;
}

// ── Cluster Parameter Groups ──────────────────────────────────────────────

public DbClusterParameterGroup createDbClusterParameterGroup(String name, String family, String description) {
if (clusterParameterGroups.get(name).isPresent()) {
throw new AwsException("DBParameterGroupAlreadyExists",
"DB cluster parameter group " + name + " already exists.", 400);
}
DbClusterParameterGroup group = new DbClusterParameterGroup(name, family, description);
clusterParameterGroups.put(name, group);
return group;
}

public DbClusterParameterGroup getDbClusterParameterGroup(String name) {
return clusterParameterGroups.get(name).orElseThrow(() ->
new AwsException("DBParameterGroupNotFound",
"DB cluster parameter group " + name + " not found.", 404));
}

public Collection<DbClusterParameterGroup> listDbClusterParameterGroups(String filterName) {
if (filterName != null && !filterName.isBlank()) {
return clusterParameterGroups.get(filterName).map(List::of).orElse(List.of());
}
return clusterParameterGroups.scan(k -> true);
}

public void deleteDbClusterParameterGroup(String name) {
if (clusterParameterGroups.get(name).isEmpty()) {
throw new AwsException("DBParameterGroupNotFound",
"DB cluster parameter group " + name + " not found.", 404);
}
clusterParameterGroups.delete(name);
}

public DbClusterParameterGroup modifyDbClusterParameterGroup(String name,
java.util.Map<String, String> parameters) {
DbClusterParameterGroup group = getDbClusterParameterGroup(name);
if (parameters != null) {
group.getParameters().putAll(parameters);
}
clusterParameterGroups.put(name, group);
return group;
}

// ── Password validation callbacks ─────────────────────────────────────────

public boolean validateDbPassword(String instanceId, String clientUser, String password) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.github.hectorvent.floci.services.rds.model;

import io.quarkus.runtime.annotations.RegisterForReflection;

import java.util.HashMap;
import java.util.Map;

@RegisterForReflection
public class DbClusterParameterGroup {

private String dbClusterParameterGroupName;
private String dbParameterGroupFamily;
private String description;
private Map<String, String> parameters = new HashMap<>();

public DbClusterParameterGroup() {}

public DbClusterParameterGroup(String dbClusterParameterGroupName, String dbParameterGroupFamily,
String description) {
this.dbClusterParameterGroupName = dbClusterParameterGroupName;
this.dbParameterGroupFamily = dbParameterGroupFamily;
this.description = description;
}

public String getDbClusterParameterGroupName() { return dbClusterParameterGroupName; }
public void setDbClusterParameterGroupName(String dbClusterParameterGroupName) { this.dbClusterParameterGroupName = dbClusterParameterGroupName; }

public String getDbParameterGroupFamily() { return dbParameterGroupFamily; }
public void setDbParameterGroupFamily(String dbParameterGroupFamily) { this.dbParameterGroupFamily = dbParameterGroupFamily; }

public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }

public Map<String, String> getParameters() { return parameters; }
public void setParameters(Map<String, String> parameters) { this.parameters = parameters; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.github.hectorvent.floci.config.EmulatorConfig;
import io.github.hectorvent.floci.core.common.AwsException;
import io.github.hectorvent.floci.services.rds.model.DbCluster;
import io.github.hectorvent.floci.services.rds.model.DbClusterParameterGroup;
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;
Expand Down Expand Up @@ -215,6 +216,77 @@ void unsupportedOperationReturnsQueryError() {
assertTrue(((String) response.getEntity()).contains("UnsupportedOperation"));
}

// ──────────────────────────── DBClusterParameterGroups ──────────────────────

@Test
void describeDbClusterParameterGroups_usesDBClusterParameterGroupTag() {
DbClusterParameterGroup group = new DbClusterParameterGroup("cpg1", "aurora-postgresql16", "test cluster group");
when(service.listDbClusterParameterGroups(null)).thenReturn(List.of(group));

Response response = handler.handle("DescribeDBClusterParameterGroups", params());

String body = (String) response.getEntity();
assertTrue(body.contains("<DBClusterParameterGroup>"), "Expected <DBClusterParameterGroup> element in response");
assertFalse(body.contains("<member><DBClusterParameterGroupName>"), "Did not expect <member> wrapping DBClusterParameterGroup");
}

@Test
void createDbClusterParameterGroup_requiresName() {
Response response = handler.handle("CreateDBClusterParameterGroup", params());

assertEquals(400, response.getStatus());
assertTrue(((String) response.getEntity()).contains("DBClusterParameterGroupName is required."));
}

@Test
void createDbClusterParameterGroup_passesArgumentsToService() {
DbClusterParameterGroup group = new DbClusterParameterGroup("cpg1", "aurora-postgresql16", "desc");
when(service.createDbClusterParameterGroup("cpg1", "aurora-postgresql16", "desc")).thenReturn(group);

MultivaluedMap<String, String> p = params();
p.add("DBClusterParameterGroupName", "cpg1");
p.add("DBParameterGroupFamily", "aurora-postgresql16");
p.add("Description", "desc");
Response response = handler.handle("CreateDBClusterParameterGroup", p);

verify(service).createDbClusterParameterGroup("cpg1", "aurora-postgresql16", "desc");
String body = (String) response.getEntity();
assertTrue(body.contains("<DBClusterParameterGroupName>cpg1</DBClusterParameterGroupName>"));
assertTrue(body.contains("<DBParameterGroupFamily>aurora-postgresql16</DBParameterGroupFamily>"));
}

@Test
void modifyDbClusterParameterGroup_ignoresParametersWithoutValue() {
DbClusterParameterGroup group = new DbClusterParameterGroup("cpg1", "aurora-postgresql16", "test group");
when(service.modifyDbClusterParameterGroup(eq("cpg1"), eq(java.util.Map.of("log_statement", "all"))))
.thenReturn(group);

MultivaluedMap<String, String> p = params();
p.add("DBClusterParameterGroupName", "cpg1");
p.add("Parameters.member.1.ParameterName", "log_statement");
p.add("Parameters.member.1.ParameterValue", "all");
p.add("Parameters.member.2.ParameterName", "ignored_without_value");
handler.handle("ModifyDBClusterParameterGroup", p);

verify(service).modifyDbClusterParameterGroup("cpg1", java.util.Map.of("log_statement", "all"));
}

@Test
void describeDbClusterParameters_requiresParameterGroupName() {
Response response = handler.handle("DescribeDBClusterParameters", params());

assertEquals(400, response.getStatus());
assertTrue(((String) response.getEntity()).contains("DBClusterParameterGroupName is required."));
}

@Test
void deleteDbClusterParameterGroup_requiresName() {
Response response = handler.handle("DeleteDBClusterParameterGroup", params());

assertEquals(400, response.getStatus());
assertTrue(((String) response.getEntity()).contains("DBClusterParameterGroupName is required."));
}

// ──────────────────────────── DBSubnetGroup shape ───────────────────────────

@Test
Expand Down
Loading
Loading