Skip to content

Commit

Permalink
fix: Fix schema import for RDS clusters in VPCs
Browse files Browse the repository at this point in the history
  • Loading branch information
palpatim authored Aug 28, 2023
1 parent 5fe85d3 commit 942ad39
Show file tree
Hide file tree
Showing 2 changed files with 254 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import {
RDSClient,
DescribeDBClustersCommandOutput,
DescribeDBInstancesCommandOutput,
DescribeDBSubnetGroupsCommandOutput,
} from '@aws-sdk/client-rds';

import { getHostVpc } from '../vpc-helper';

const sendSpy = jest.spyOn(RDSClient.prototype, 'send');

const vpcId = 'vpc-aaaaaaaaaaaaaaaaa';
const subnetIds = ['subnet-1111111111', 'subnet-2222222222'];
const securityGroupIds = ['sg-abc123'];

describe('detect VPC settings', () => {
it('should detect VPC settings for an RDS instance', async () => {
// TS complains about resolving values in the spy.
// Cast it through never to overcome the compile error.
sendSpy.mockResolvedValueOnce(instanceResponse as never);

const result = await getHostVpc('mock-rds-cluster-instance-1.aaaaaaaaaaaa.us-west-2.rds.amazonaws.com', 'us-west-2');

expect(result).toBeDefined();
expect(result?.vpcId).toEqual(vpcId);
expect(result?.subnetIds).toEqual(expect.arrayContaining(subnetIds));
expect(result?.subnetIds.length).toEqual(subnetIds.length);
expect(result?.securityGroupIds).toEqual(expect.arrayContaining(securityGroupIds));
expect(result?.securityGroupIds.length).toEqual(securityGroupIds.length);
});

it('should detect VPC settings for an RDS cluster', async () => {
// TS complains about resolving values in the spy.
// Cast it through never to overcome the compile error.
sendSpy
.mockResolvedValueOnce(instanceResponse as never)
.mockResolvedValueOnce(clusterResponse as never)
.mockResolvedValueOnce(subnetResponse as never);

const result = await getHostVpc('mock-rds-cluster.cluster-abc123.us-west-2.rds.amazonaws.com', 'us-west-2');

expect(result).toBeDefined();
expect(result?.vpcId).toEqual(vpcId);
expect(result?.subnetIds).toEqual(expect.arrayContaining(subnetIds));
expect(result?.subnetIds.length).toEqual(subnetIds.length);
expect(result?.securityGroupIds).toEqual(expect.arrayContaining(securityGroupIds));
expect(result?.securityGroupIds.length).toEqual(securityGroupIds.length);
});
});

const instanceResponse: DescribeDBInstancesCommandOutput = {
$metadata: {},
DBInstances: [
{
DBInstanceIdentifier: 'mock-rds-cluster-instance-1',
DBInstanceClass: 'db.serverless',
Engine: 'aurora-mysql',
DBInstanceStatus: 'available',
MasterUsername: 'admin',
Endpoint: {
Address: 'mock-rds-cluster-instance-1.aaaaaaaaaaaa.us-west-2.rds.amazonaws.com',
Port: 3306,
HostedZoneId: 'Z1AAAAAAAAAAAA',
},
AllocatedStorage: 1,
InstanceCreateTime: new Date(),
PreferredBackupWindow: '09:08-09:38',
BackupRetentionPeriod: 1,
DBSecurityGroups: [],
VpcSecurityGroups: [
{
VpcSecurityGroupId: securityGroupIds[0],
Status: 'active',
},
],
DBParameterGroups: [
{
DBParameterGroupName: 'default.aurora-mysql8.0',
ParameterApplyStatus: 'in-sync',
},
],
AvailabilityZone: 'us-west-2b',
DBSubnetGroup: {
DBSubnetGroupName: 'default-vpc-abc123',
DBSubnetGroupDescription: 'Created from the RDS Management Console',
VpcId: vpcId,
SubnetGroupStatus: 'Complete',
Subnets: [
{
SubnetIdentifier: subnetIds[0],
SubnetAvailabilityZone: {
Name: 'us-west-2b',
},
SubnetOutpost: {},
SubnetStatus: 'Active',
},
{
SubnetIdentifier: subnetIds[1],
SubnetAvailabilityZone: {
Name: 'us-west-2b',
},
SubnetOutpost: {},
SubnetStatus: 'Active',
},
],
},
PreferredMaintenanceWindow: 'sat:06:40-sat:07:10',
PendingModifiedValues: {},
MultiAZ: false,
EngineVersion: '8.0.mysql_aurora.3.04.0',
AutoMinorVersionUpgrade: true,
ReadReplicaDBInstanceIdentifiers: [],
LicenseModel: 'general-public-license',
OptionGroupMemberships: [
{
OptionGroupName: 'default:aurora-mysql-8-0',
Status: 'in-sync',
},
],
PubliclyAccessible: true,
StorageType: 'aurora',
DbInstancePort: 0,
DBClusterIdentifier: 'mock-rds-cluster',
StorageEncrypted: true,
KmsKeyId: 'arn:aws:kms:us-west-2:123456789012:key/11112222-3333-4444-aaaa-bbbbbbbbbbbb',
DbiResourceId: 'db-IDAAAAAAAAAAAAAAAAAAAAAAAA',
CACertificateIdentifier: 'rds-ca-2019',
DomainMemberships: [],
CopyTagsToSnapshot: false,
MonitoringInterval: 60,
EnhancedMonitoringResourceArn: 'arn:aws:logs:us-west-2:123456789012:log-group:RDSOSMetrics:log-stream:db-IDAAAAAAAAAAAAAAAAAAAAAAAA',
MonitoringRoleArn: 'arn:aws:iam::123456789012:role/rds-monitoring-role',
PromotionTier: 1,
DBInstanceArn: 'arn:aws:rds:us-west-2:123456789012:db:mock-rds-cluster-instance-1',
IAMDatabaseAuthenticationEnabled: false,
PerformanceInsightsEnabled: true,
PerformanceInsightsKMSKeyId: 'arn:aws:kms:us-west-2:123456789012:key/11112222-3333-4444-aaaa-bbbbbbbbbbbb',
PerformanceInsightsRetentionPeriod: 7,
DeletionProtection: false,
AssociatedRoles: [],
TagList: [],
CustomerOwnedIpEnabled: false,
BackupTarget: 'region',
NetworkType: 'IPV4',
},
],
};

const clusterResponse: DescribeDBClustersCommandOutput = {
$metadata: {},
DBClusters: [
{
AllocatedStorage: 1,
AvailabilityZones: ['us-west-2c', 'us-west-2b', 'us-west-2a'],
BackupRetentionPeriod: 1,
DBClusterIdentifier: 'mock-rds-cluster',
DBClusterParameterGroup: 'default.aurora-mysql8.0',
DBSubnetGroup: 'default-vpc-abc123',
Status: 'available',
EarliestRestorableTime: new Date(),
Endpoint: 'mock-rds-cluster.cluster-abc123.us-west-2.rds.amazonaws.com',
ReaderEndpoint: 'mock-rds-cluster.cluster.cluster-ro-abc123.us-west-2.rds.amazonaws.com',
MultiAZ: false,
Engine: 'aurora-mysql',
EngineVersion: '8.0.mysql_aurora.3.04.0',
LatestRestorableTime: new Date(),
Port: 3306,
MasterUsername: 'admin',
PreferredBackupWindow: '09:08-09:38',
PreferredMaintenanceWindow: 'thu:06:19-thu:06:49',
ReadReplicaIdentifiers: [],
DBClusterMembers: [
{
DBInstanceIdentifier: 'mock-rds-cluster-instance-1',
IsClusterWriter: true,
DBClusterParameterGroupStatus: 'in-sync',
PromotionTier: 1,
},
],
VpcSecurityGroups: [
{
VpcSecurityGroupId: securityGroupIds[0],
Status: 'active',
},
],
HostedZoneId: 'Z1AAAA0A000A0A',
StorageEncrypted: true,
KmsKeyId: 'arn:aws:kms:us-west-2:123456789012:key/11112222-3333-4444-aaaa-bbbbbbbbbbbb',
DbClusterResourceId: 'cluster-00AAAAAAAAAAAAAAAAAAAAAAAA',
DBClusterArn: 'arn:aws:rds:us-west-2:123456789012:cluster:mock-rds-cluster',
AssociatedRoles: [],
IAMDatabaseAuthenticationEnabled: false,
ClusterCreateTime: new Date(),
EngineMode: 'provisioned',
DeletionProtection: true,
HttpEndpointEnabled: false,
ActivityStreamStatus: 'stopped',
CopyTagsToSnapshot: true,
CrossAccountClone: false,
DomainMemberships: [],
TagList: [],
AutoMinorVersionUpgrade: true,
ServerlessV2ScalingConfiguration: {
MinCapacity: 8,
MaxCapacity: 64,
},
NetworkType: 'IPV4',
},
],
};

const subnetResponse: DescribeDBSubnetGroupsCommandOutput = {
$metadata: {},
DBSubnetGroups: [
{
DBSubnetGroupName: 'default-vpc-abc123',
DBSubnetGroupDescription: 'Created from the RDS Management Console',
VpcId: vpcId,
SubnetGroupStatus: 'Complete',
Subnets: [
{
SubnetIdentifier: subnetIds[0],
SubnetAvailabilityZone: {
Name: 'us-west-2b',
},
SubnetOutpost: {},
SubnetStatus: 'Active',
},
{
SubnetIdentifier: subnetIds[1],
SubnetAvailabilityZone: {
Name: 'us-west-2b',
},
SubnetOutpost: {},
SubnetStatus: 'Active',
},
],
DBSubnetGroupArn: 'arn:aws:rds:us-west-2:123456789012:subgrp:default-vpc-abc123',
SupportedNetworkTypes: ['IPV4'],
},
],
};
24 changes: 12 additions & 12 deletions packages/amplify-graphql-schema-generator/src/utils/vpc-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ const getSubnetIds = async (
* @param region AWS region.
*/
export const getHostVpc = async (hostname: string, region: string): Promise<VpcConfig | undefined> =>
checkHostInDBInstances(hostname, region) ?? checkHostInDBClusters(hostname, region);
(await checkHostInDBInstances(hostname, region)) ?? (await checkHostInDBClusters(hostname, region));

/**
* Provisions a lambda function to introspect the database schema.
Expand All @@ -150,7 +150,7 @@ export const getHostVpc = async (hostname: string, region: string): Promise<VpcC
export const provisionSchemaInspectorLambda = async (lambdaName: string, vpc: VpcConfig, region: string): Promise<void> => {
const roleName = `${lambdaName}-execution-role`;
let createLambda = true;
const iamRole = await createRoleIfNotExists(roleName);
const iamRole = await createRoleIfNotExists(roleName, region);
const existingLambda = await getSchemaInspectorLambda(lambdaName, region);
spinner.start('Provisioning a function to introspect the database schema...');
try {
Expand Down Expand Up @@ -237,13 +237,13 @@ const updateSchemaInspectorLambda = async (lambdaName: string, region: string):
await lambdaClient.send(new UpdateFunctionCodeCommand(params));
};

const createRoleIfNotExists = async (roleName): Promise<Role> => {
let role = await getRole(roleName);
const createRoleIfNotExists = async (roleName: string, region: string): Promise<Role> => {
let role = await getRole(roleName, region);
// Wait for role created with SDK to propagate.
// Otherwise it will throw error "The role defined for the function cannot be assumed by Lambda" while creating the lambda.
const ROLE_PROPAGATION_DELAY = 10000;
if (!role) {
role = await createRole(roleName);
role = await createRole(roleName, region);
await sleep(ROLE_PROPAGATION_DELAY);
}
return role;
Expand All @@ -255,8 +255,8 @@ const createRoleIfNotExists = async (roleName): Promise<Role> => {
*/
export const sleep = async (milliseconds: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, milliseconds));

const createPolicy = async (policyName: string): Promise<Policy | undefined> => {
const client = new IAMClient({});
const createPolicy = async (policyName: string, region: string): Promise<Policy | undefined> => {
const client = new IAMClient({ region });
const command = new CreatePolicyCommand({
PolicyName: policyName,
PolicyDocument: JSON.stringify({
Expand All @@ -275,9 +275,9 @@ const createPolicy = async (policyName: string): Promise<Policy | undefined> =>
return result.Policy;
};

const createRole = async (roleName): Promise<Role | undefined> => {
const client = new IAMClient({});
const policy = await createPolicy(`${roleName}-policy`);
const createRole = async (roleName: string, region: string): Promise<Role | undefined> => {
const client = new IAMClient({ region });
const policy = await createPolicy(`${roleName}-policy`, region);
const command = new CreateRoleCommand({
AssumeRolePolicyDocument: JSON.stringify({
Version: '2012-10-17',
Expand All @@ -304,8 +304,8 @@ const createRole = async (roleName): Promise<Role | undefined> => {
return result.Role;
};

const getRole = async (roleName): Promise<Role | undefined> => {
const client = new IAMClient({});
const getRole = async (roleName: string, region: string): Promise<Role | undefined> => {
const client = new IAMClient({ region });
const command = new GetRoleCommand({
RoleName: roleName,
});
Expand Down

0 comments on commit 942ad39

Please sign in to comment.