in resources/custom-resources/rds-options/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/RdsOptions.java [63:253]
public Object handleRequest(Map<String, Object> event, Context context) {
Utils.logRequestEvent(event);
final String requestType = (String) event.get("RequestType");
Map<String, Object> resourceProperties = (Map<String, Object>) event.get("ResourceProperties");
final String table = (String) resourceProperties.get("Table");
ExecutorService service = Executors.newSingleThreadExecutor();
ObjectNode responseData = JsonNodeFactory.instance.objectNode();
try {
Runnable r = () -> {
if ("Create".equalsIgnoreCase(requestType)) {
LOGGER.info("CREATE");
// The same RDS engine version will be available for multiple EC2 instance types so we'll
// keep a working set of them to limit the number of describeDBEngineVersions calls
LOGGER.info("Building options for {}", AWS_REGION);
// For every RDS engine
for (Database.RDS_ENGINE engine : Database.RDS_ENGINE.values()) {
Map<String, Map<String, Object>> engineVersionCache = new HashMap<>();
//for (Database.RDS_ENGINE engine : new Database.RDS_ENGINE[] {Database.RDS_ENGINE.AURORA_PG}) {
LOGGER.info("RDS engine {}", engine.name());
// Each engine needs its own copy of the list of instance types because
// they can be different by engine by region
EnumMap<Database.RDS_INSTANCE, Map<String, Object>> instanceMap = new EnumMap<>(Database.RDS_INSTANCE.class);
for (Database.RDS_INSTANCE inst : Database.RDS_INSTANCE.values()) {
//for (Database.RDS_INSTANCE inst : new Database.RDS_INSTANCE[] {Database.RDS_INSTANCE.T3_MEDIUM}) {
Map<String, Object> instanceDetails = new HashMap<>();
instanceDetails.put("class", inst.getInstanceClass());
instanceDetails.put("description", inst.getDescription());
instanceDetails.put("versions", new ArrayList<Map<String, Object>>());
instanceMap.put(inst, instanceDetails);
}
Map<String, Object> engineDetails = new HashMap<>();
engineDetails.put("name", engine.getEngine());
engineDetails.put("description", engine.getDescription());
engineDetails.put("instances", instanceMap);
// For every RDS database instance type
for (Map.Entry<Database.RDS_INSTANCE, Map<String, Object>> instance : instanceMap.entrySet()) {
String instanceClass = (String) instance.getValue().get("class");
LOGGER.info("RDS instance class {}", instanceClass);
List<Map<String, Object>> versions = new ArrayList<>();
Integer maxRecords = 100;
String marker = null;
do {
try {
DescribeOrderableDbInstanceOptionsResponse orderableResponse = rds.describeOrderableDBInstanceOptions(DescribeOrderableDbInstanceOptionsRequest.builder()
.engine(engine.getEngine())
.dbInstanceClass(instanceClass)
.vpc(Boolean.TRUE)
.marker(marker)
.maxRecords(maxRecords)
.build()
);
LOGGER.info("{} {} {} has {} orderable instance options", AWS_REGION.toString(), engine.getEngine(), instanceClass, orderableResponse.orderableDBInstanceOptions().size());
marker = orderableResponse.marker();
// We can get more orderable instances than versions due to storage, IOPs,
// security, networking, and other options. We are just interested in the
// unique set of engine versions per instance type.
//LOGGER.info("Processing {} instance options", orderableResponse.orderableDBInstanceOptions().size());
for (OrderableDBInstanceOption orderable : orderableResponse.orderableDBInstanceOptions()) {
String orderableVersion = orderable.engineVersion();
// Have we already seen this version for this instance type?
boolean newVersion = versions.stream()
.filter(m -> orderableVersion.equals(m.get("version")))
.collect(Collectors.toList())
.isEmpty();
// If we haven't seen this version yet for this instance type
if (newVersion) {
// Maybe we've seen this same version for other instance types
boolean cachedVersion = engineVersionCache.containsKey(orderableVersion);
if (!cachedVersion) {
// Nope... make the call to get the details for this version
//LOGGER.info("Engine version {}", orderableVersion);
DescribeDbEngineVersionsResponse versionsResponse = rds.describeDBEngineVersions(DescribeDbEngineVersionsRequest.builder()
.engine(engine.getEngine())
.engineVersion(orderableVersion)
.build()
);
//LOGGER.info("Processing {} engine versions", versionsResponse.dbEngineVersions().size());
for (DBEngineVersion dbVersion : versionsResponse.dbEngineVersions()) {
// describeDBEngineVersions brings back multiple results with the same
// version for at least db.t3.medium aurora-postgresql (bug?)
boolean uniqueVersionResponse = versions.stream()
.filter(m -> dbVersion.engineVersion().equals(m.get("version")))
.collect(Collectors.toList())
.isEmpty();
if (uniqueVersionResponse) {
Map<String, Object> version = new HashMap<>();
version.put("version", dbVersion.engineVersion());
version.put("description", dbVersion.dbEngineVersionDescription());
version.put("family", dbVersion.dbParameterGroupFamily());
//LOGGER.info("Adding version {}", version.toString());
versions.add(version);
// Be sure to cache the results
//LOGGER.info("Caching version {}", dbVersion.engineVersion());
engineVersionCache.put(dbVersion.engineVersion(), version);
} else {
//LOGGER.info("Skipping duplicate db version response");
}
}
} else {
// We haven't seen this version for this instance type, but we have for
// other instance types for this RDS engine, so we can use our cached copy
// of the version response.
//LOGGER.info("Engine version {} (cached)", orderableVersion);
versions.add(engineVersionCache.get(orderableVersion));
}
}
}
} catch (SdkServiceException rdsError) {
LOGGER.error("rds:describeOrderableDBInstanceOptions | rds:DescribeDbEngineVersions error {}", rdsError.getMessage());
String stackTrace = Utils.getFullStackTrace(rdsError);
LOGGER.error(stackTrace);
responseData.put("Reason", stackTrace);
sendResponse(event, context, "FAILED", responseData);
}
} while (marker != null && !marker.isEmpty());
// Now we have all the unique versions for this instance type
LOGGER.info("{} {} {} has {} available versions", AWS_REGION.toString(), engine.getEngine(), instanceClass, versions.size());
instanceMap.get(instance.getKey()).put("versions", versions);
}
// Remove instances that don't have any orderable versions
for (Database.RDS_INSTANCE instance : instanceMap.keySet()) {
if (((List) instanceMap.get(instance).get("versions")).isEmpty()) {
LOGGER.info("Removing unavailable instance class {} for {} in {}", instance.getInstanceClass(), engine.getEngine(), AWS_REGION.toString());
instanceMap.remove(instance);
}
}
if (((Map) engineDetails.get("instances")).isEmpty()) {
LOGGER.info("Removing unavailable engine {} in {}", engine.getEngine(), AWS_REGION.toString());
} else {
// Save this engine's options for this region to our database for fast lookup
try {
Map<String, AttributeValue> item = new HashMap<>();
item.put("region", AttributeValue.builder().s(AWS_REGION).build());
item.put("engine", AttributeValue.builder().s(engine.name()).build());
item.put("options", AttributeValue.builder().m(toAttributeValueMap(engineDetails)).build());
PutItemResponse response = ddb.putItem(request -> request.tableName(table).item(item));
} catch(SdkServiceException ddbError) {
LOGGER.error("dynamodb:putItem error {}", ddbError.getMessage());
String stackTrace = Utils.getFullStackTrace(ddbError);
LOGGER.error(stackTrace);
responseData.put("Reason", stackTrace);
sendResponse(event, context, "FAILED", responseData);
}
}
}
// Tell CloudFormation we're done
sendResponse(event, context, "SUCCESS", responseData);
} else if ("Update".equalsIgnoreCase(requestType)) {
LOGGER.info("UPDATE");
sendResponse(event, context, "SUCCESS", responseData);
} else if ("Delete".equalsIgnoreCase(requestType)) {
LOGGER.info("DELETE");
sendResponse(event, context, "SUCCESS", responseData);
} else {
LOGGER.error("FAILED unknown requestType " + requestType);
responseData.put("Reason", "Unknown RequestType " + requestType);
sendResponse(event, context, "FAILED", responseData);
}
};
Future<?> f = service.submit(r);
f.get(context.getRemainingTimeInMillis() - 1000, TimeUnit.MILLISECONDS);
} catch (final TimeoutException | InterruptedException | ExecutionException e) {
// Timed out
LOGGER.error("FAILED unexpected error or request timed out " + e.getMessage());
String stackTrace = Utils.getFullStackTrace(e);
LOGGER.error(stackTrace);
responseData.put("Reason", stackTrace);
sendResponse(event, context, "FAILED", responseData);
} finally {
service.shutdown();
}
return null;
}