@aws-amplify/amplify-category-api
Version:
Amplify CLI API Category Plugin
293 lines (235 loc) • 9.8 kB
text/typescript
import * as path from 'path';
import {
$TSContext,
exitOnNextTick,
ResourceCredentialsNotFoundError,
ResourceDoesNotExistError,
pathManager,
JSONUtilities,
} from '@aws-amplify/amplify-cli-core';
import { printer, prompter } from '@aws-amplify/amplify-prompts';
import chalk from 'chalk';
import { DataApiParams } from 'graphql-relational-schema-transformer';
import ora from 'ora';
import { rootStackFileName } from '@aws-amplify/amplify-provider-awscloudformation';
const spinner = ora('');
const category = 'api';
const providerName = 'awscloudformation';
export async function serviceWalkthrough(context: $TSContext, datasourceMetadata: Record<string, any>) {
const amplifyMeta = context.amplify.getProjectMeta();
// Verify that an API exists in the project before proceeding.
if (amplifyMeta == null || amplifyMeta[category] == null || Object.keys(amplifyMeta[category]).length === 0) {
const errMessage =
'You must create an AppSync API in your project before adding a graphql datasource. Please use "amplify api add" to create the API.';
printer.error(errMessage);
await context.usageData.emitError(new ResourceDoesNotExistError(errMessage));
exitOnNextTick(0);
}
// Loop through to find the AppSync API Resource Name
let appSyncApi: string;
const apis = Object.keys(amplifyMeta[category]);
for (const api of apis) {
if (amplifyMeta[category][api].service === 'AppSync') {
appSyncApi = api;
break;
}
}
// If an AppSync API does not exist, inform the user to create the AppSync API
if (!appSyncApi) {
const errMessage =
'You must create an AppSync API in your project before adding a graphql datasource. Please use "amplify api add" to create the API.';
printer.error(errMessage);
await context.usageData.emitError(new ResourceDoesNotExistError(errMessage));
exitOnNextTick(0);
}
const { inputs, availableRegions } = datasourceMetadata;
// FIXME: We should NOT be treating CloudFormation templates as inputs to prompts! This a temporary exception while we move team-provider-info to a service.
const cfnJson: any = JSONUtilities.readJson(
path.join(pathManager.getCurrentCloudRootStackDirPath(pathManager.findProjectRoot()), rootStackFileName),
);
const cfnJsonParameters = cfnJson?.Resources[`api${appSyncApi}`]?.Properties?.Parameters || {};
let selectedRegion = cfnJsonParameters?.rdsRegion;
// Region Question
if (!selectedRegion) {
selectedRegion = await promptWalkthroughQuestion(inputs, 0, availableRegions);
}
const AWS = await getAwsClient(context, 'list');
// Prepare the SDK with the region
AWS.config.update({
region: selectedRegion,
});
// RDS Cluster Question
let selectedClusterArn = cfnJsonParameters?.rdsClusterIdentifier;
let clusterResourceId = getRdsClusterResourceIdFromArn(selectedClusterArn, AWS);
if (!selectedClusterArn || !clusterResourceId) {
({ selectedClusterArn, clusterResourceId } = await selectCluster(context, inputs, AWS));
}
// Secret Store Question
let selectedSecretArn = cfnJsonParameters?.rdsSecretStoreArn;
if (!selectedSecretArn) {
selectedSecretArn = await getSecretStoreArn(context, inputs, clusterResourceId, AWS);
}
// Database Name Question
let selectedDatabase = cfnJsonParameters?.rdsDatabaseName;
if (!selectedDatabase) {
selectedDatabase = await selectDatabase(context, inputs, selectedClusterArn, selectedSecretArn, AWS);
}
return {
region: selectedRegion,
dbClusterArn: selectedClusterArn,
secretStoreArn: selectedSecretArn,
databaseName: selectedDatabase,
resourceName: appSyncApi,
};
}
async function getRdsClusterResourceIdFromArn(arn: string | undefined, AWS) {
// If the arn was not already existing in cloudformation template, return undefined to prompt for input.
if (!arn) {
return;
}
const RDS = new AWS.RDS();
const describeDBClustersResult = await RDS.describeDBClusters().promise();
const rawClusters = describeDBClustersResult.DBClusters;
const identifiedCluster = rawClusters.find((cluster) => cluster.DBClusterArn === arn);
return identifiedCluster.DBClusterIdentifier;
}
/**
*
* @param {*} inputs
*/
async function selectCluster(context: $TSContext, inputs, AWS) {
const RDS = new AWS.RDS();
const describeDBClustersResult = await RDS.describeDBClusters().promise();
const rawClusters = describeDBClustersResult.DBClusters;
const clusters = new Map();
const serverlessClusters = rawClusters.filter((cluster) => cluster.EngineMode === 'serverless');
if (serverlessClusters.length === 0) {
const errMessage = 'No properly configured Aurora Serverless clusters found.';
printer.error(errMessage);
await context.usageData.emitError(new ResourceDoesNotExistError(errMessage));
exitOnNextTick(0);
}
for (const cluster of serverlessClusters) {
clusters.set(cluster.DBClusterIdentifier, cluster);
}
if (clusters.size > 1) {
const clusterIdentifier = await promptWalkthroughQuestion(inputs, 1, Array.from(clusters.keys()));
const selectedCluster = clusters.get(clusterIdentifier);
return {
selectedClusterArn: selectedCluster.DBClusterArn,
clusterResourceId: selectedCluster.DbClusterResourceId,
};
}
// Pick first and only value
const firstCluster = Array.from(clusters.values())[0];
printer.info(`${chalk.green('✔')} Only one Cluster was found: '${firstCluster.DBClusterIdentifier}' was automatically selected.`);
return {
selectedClusterArn: firstCluster.DBClusterArn,
clusterResourceId: firstCluster.DbClusterResourceId,
};
}
/**
*
* @param {*} inputs
* @param {*} clusterResourceId
*/
async function getSecretStoreArn(context: $TSContext, inputs, clusterResourceId, AWS) {
const SecretsManager = new AWS.SecretsManager();
const NextToken = 'NextToken';
let rawSecrets = [];
const params = {
MaxResults: 20,
};
const listSecretsResult = await SecretsManager.listSecrets(params).promise();
rawSecrets = listSecretsResult.SecretList;
let token = listSecretsResult.NextToken;
while (token) {
params[NextToken] = token;
const tempSecretsResult = await SecretsManager.listSecrets(params).promise();
rawSecrets = [...rawSecrets, ...tempSecretsResult.SecretList];
token = tempSecretsResult.NextToken;
}
const secrets = new Map();
const secretsForCluster = rawSecrets.filter((secret) => secret.Name.startsWith(`rds-db-credentials/${clusterResourceId}`));
if (secretsForCluster.length === 0) {
const errMessage = 'No RDS access credentials found in the AWS Secrect Manager.';
printer.error(errMessage);
await context.usageData.emitError(new ResourceCredentialsNotFoundError(errMessage));
exitOnNextTick(0);
}
for (const secret of secretsForCluster) {
secrets.set(secret.Name, secret.ARN);
}
let selectedSecretArn;
if (secrets.size > 1) {
// Kick off questions flow
const selectedSecretName = await promptWalkthroughQuestion(inputs, 2, Array.from(secrets.keys()));
selectedSecretArn = secrets.get(selectedSecretName);
} else {
// Pick first and only value
selectedSecretArn = Array.from(secrets.values())[0];
printer.info(`${chalk.green('✔')} Only one Secret was found for the cluster: '${selectedSecretArn}' was automatically selected.`);
}
return selectedSecretArn;
}
/**
*
* @param {*} inputs
* @param {*} clusterArn
* @param {*} secretArn
*/
async function selectDatabase(context: $TSContext, inputs, clusterArn, secretArn, AWS) {
// Database Name Question
const DataApi = new AWS.RDSDataService();
const params = new DataApiParams();
const databaseList = [];
params.secretArn = secretArn;
params.resourceArn = clusterArn;
params.sql = 'SHOW databases';
spinner.start('Fetching Aurora Serverless cluster...');
try {
const dataApiResult = await DataApi.executeStatement(params).promise();
const excludedDatabases = ['information_schema', 'performance_schema', 'mysql', 'sys'];
databaseList.push(...dataApiResult.records.map((record) => record[0].stringValue).filter((name) => !excludedDatabases.includes(name)));
spinner.succeed('Fetched Aurora Serverless cluster.');
} catch (err) {
spinner.fail(err.message);
if (err.code === 'BadRequestException' && /Access denied for user/.test(err.message)) {
const msg =
`Ensure that '${secretArn}' contains your database credentials. ` +
'Please note that Aurora Serverless does not support IAM database authentication.';
printer.error(msg);
}
}
if (databaseList.length === 0) {
const errMessage = 'No database found in the selected cluster.';
printer.error(errMessage);
await context.usageData.emitError(new ResourceDoesNotExistError(errMessage));
exitOnNextTick(0);
}
if (databaseList.length > 1) {
return await promptWalkthroughQuestion(inputs, 3, databaseList);
}
printer.info(`${chalk.green('✔')} Only one Database was found: '${databaseList[0]}' was automatically selected.`);
return databaseList[0];
}
/**
*
* @param {*} inputs
* @param {*} questionNumber
* @param {*} choicesList
*/
async function promptWalkthroughQuestion(inputs, questionNumber, choicesList) {
const question = {
type: inputs[questionNumber].type,
name: inputs[questionNumber].key,
message: inputs[questionNumber].question,
choices: choicesList,
};
return await prompter.pick(question.message, choicesList);
}
async function getAwsClient(context: $TSContext, action: string) {
const providerPlugins = context.amplify.getProviderPlugins(context);
const provider = require(providerPlugins[providerName]);
return await provider.getConfiguredAWSClient(context, 'aurora-serverless', action);
}