@aws-amplify/amplify-category-api
Version:
Amplify CLI API Category Plugin
359 lines (309 loc) • 11.7 kB
text/typescript
import { $TSContext, exitOnNextTick, ResourceDoesNotExistError } from '@aws-amplify/amplify-cli-core';
import { printer } from '@aws-amplify/amplify-prompts';
import inquirer from 'inquirer';
import { category } from '../../../category-constants';
import { DEPLOYMENT_MECHANISM } from '../base-api-stack';
import { GitHubSourceActionInfo } from '../pipeline-with-awaiter';
import { getAllDefaults } from '../default-values/containers-defaults';
const serviceName = 'ElasticContainer';
/**
* Designed to be backwards compatible with the old way of representing dependencies as
* {
* category: string
* resourceName: string
* attributes: string[]
* }
* and auto-generating environment variable names based on this info
* When attributeEnvMap is specified, it can specify a custom environment variable name for a dependency attribute
* If no mapping is found for an attribute in the map, then it falls back to the autogenerated value
*/
export interface ResourceDependency {
category: string; // resource category of the dependency
resourceName: string; // name of the dependency
attributes: string[]; // attributes that this function depends on (must be outputs of the dependencies CFN template)
attributeEnvMap?: { [name: string]: string }; // optional attributes to environment variable names map that will be exposed to the function
}
export enum API_TYPE {
GRAPHQL = 'GRAPHQL',
REST = 'REST',
}
export type ServiceConfiguration = {
resourceName: string;
imageSource: { type: IMAGE_SOURCE_TYPE; template?: string };
gitHubPath: string;
authName: string;
gitHubToken: string;
deploymentMechanism: DEPLOYMENT_MECHANISM;
restrictAccess: boolean;
dependsOn?: ResourceDependency[]; // resources this function depends on
categoryPolicies?: any[]; // IAM policies that should be applied to this lambda
mutableParametersState?: any; // Contains the object that is written to function-parameters.json. Kindof a hold-over from older code
environmentMap?: Record<string, any>; // Existing function environment variable map. Should refactor to use dependsOn directly,
gitHubInfo?: GitHubSourceActionInfo;
};
export async function serviceWalkthrough(context: $TSContext, apiType: API_TYPE): Promise<Partial<ServiceConfiguration>> {
const allDefaultValues = getAllDefaults();
const resourceName = await askResourceName(context, allDefaultValues);
const containerInfo = await askContainerSource(context, resourceName, apiType);
return { resourceName, ...containerInfo };
}
async function askResourceName(context: $TSContext, allDefaultValues: Record<string, any>) {
const { amplify } = context;
const { resourceName } = await inquirer.prompt([
{
name: 'resourceName',
type: 'input',
message: 'Provide a friendly name for your resource to be used as a label for this category in the project:',
default: allDefaultValues.resourceName,
validate: amplify.inputValidation({
validation: {
operator: 'regex',
value: '^[a-z0-9]+$',
onErrorMsg: 'Resource name should be alphanumeric with no uppercase letters',
},
required: true,
}),
},
]);
return resourceName;
}
async function askContainerSource(context: $TSContext, resourceName: string, apiType: API_TYPE): Promise<Partial<ServiceConfiguration>> {
return newContainer(context, resourceName, apiType);
}
export enum IMAGE_SOURCE_TYPE {
TEMPLATE = 'TEMPLATE',
CUSTOM = 'CUSTOM',
}
async function newContainer(context: $TSContext, resourceName: string, apiType: API_TYPE): Promise<Partial<ServiceConfiguration>> {
let imageSource: { type: IMAGE_SOURCE_TYPE; template?: string };
let choices = [];
if (apiType === API_TYPE.GRAPHQL) {
choices.push({
name: 'ExpressJS - GraphQL template',
value: { type: IMAGE_SOURCE_TYPE.TEMPLATE, template: 'graphql-express' },
});
}
if (apiType === API_TYPE.REST) {
choices.push({
name: 'ExpressJS - REST template',
value: { type: IMAGE_SOURCE_TYPE.TEMPLATE, template: 'dockerfile-rest-express' },
});
choices.push({
name: 'Docker Compose - ExpressJS + Flask template',
value: { type: IMAGE_SOURCE_TYPE.TEMPLATE, template: 'dockercompose-rest-express' },
});
}
choices = choices.concat([
{
name: 'Custom (bring your own Dockerfile or docker-compose.yml)',
value: { type: IMAGE_SOURCE_TYPE.CUSTOM },
},
{
name: 'Learn More',
value: undefined,
},
]);
do {
({ imageSource } = await inquirer.prompt([
{
name: 'imageSource',
type: 'list',
message: 'What image would you like to use',
choices,
default: 'express_hello_world',
},
]));
} while (imageSource === undefined);
let deploymentMechanismQuestion;
const deploymentMechanismChoices = [
{
name: 'On every "amplify push" (Fully managed container source)',
value: DEPLOYMENT_MECHANISM.FULLY_MANAGED,
},
];
if (imageSource.type === IMAGE_SOURCE_TYPE.CUSTOM) {
deploymentMechanismChoices.push({
name: 'On every Github commit (Independently managed container source)',
value: DEPLOYMENT_MECHANISM.INDENPENDENTLY_MANAGED,
});
}
deploymentMechanismChoices.push({
name: 'Advanced: Self-managed (Learn more: docs.amplify.aws/cli/usage/containers)',
value: DEPLOYMENT_MECHANISM.SELF_MANAGED,
});
do {
deploymentMechanismQuestion = await inquirer.prompt([
{
name: 'deploymentMechanism',
type: 'list',
message: 'When do you want to build & deploy the Fargate task',
choices: deploymentMechanismChoices,
},
]);
} while (deploymentMechanismQuestion.deploymentMechanism === 'Learn More');
let gitHubPath: string;
let gitHubToken: string;
if (deploymentMechanismQuestion.deploymentMechanism === DEPLOYMENT_MECHANISM.INDENPENDENTLY_MANAGED) {
printer.info('We need a Github Personal Access Token to automatically build & deploy your Fargate task on every Github commit.');
printer.info(
'Learn more about Github Personal Access Token here: https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token',
);
const gitHubQuestions = await inquirer.prompt([
{
name: 'github_access_token',
type: 'password',
message: 'GitHub Personal Access Token:',
},
{
name: 'github_path',
type: 'input',
message: 'Path to your repo:',
},
]);
gitHubPath = gitHubQuestions.github_path;
gitHubToken = gitHubQuestions.github_access_token;
}
const meta = context.amplify.getProjectDetails().amplifyMeta;
const hasAccessableResources = ['storage', 'function'].some((categoryName) => {
return Object.keys(meta[categoryName] ?? {}).length > 0;
});
let rolePermissions: any = {};
if (
hasAccessableResources &&
(await context.amplify.confirmPrompt('Do you want to access other resources in this project from your api?'))
) {
rolePermissions = await context.amplify.invokePluginMethod(context, 'function', undefined, 'askExecRolePermissionsQuestions', [
context,
resourceName,
undefined,
undefined,
category,
serviceName,
]);
}
const { categoryPolicies, environmentMap, dependsOn, mutableParametersState } = rolePermissions;
const restrictApiQuestion = await inquirer.prompt({
name: 'rescrict_access',
type: 'confirm',
message: 'Do you want to restrict API access',
default: true,
});
return {
imageSource,
gitHubPath,
gitHubToken,
deploymentMechanism: deploymentMechanismQuestion.deploymentMechanism,
restrictAccess: restrictApiQuestion.rescrict_access,
categoryPolicies,
environmentMap,
dependsOn,
mutableParametersState,
};
}
export async function updateWalkthrough(context: $TSContext, apiType: API_TYPE) {
const { allResources } = await context.amplify.getResourceStatus();
const resources = allResources
.filter(
(resource) =>
resource.category === category && resource.service === serviceName && !!resource.providerPlugin && resource.apiType === apiType,
)
.map((resource) => resource.resourceName);
// There can only be one appsync resource
if (resources.length === 0) {
const errMessage = `No ${apiType} API resource to update. Use "amplify add api" command to create a new ${apiType} API`;
printer.error(errMessage);
await context.usageData.emitError(new ResourceDoesNotExistError(errMessage));
exitOnNextTick(0);
return;
}
const question = [
{
name: 'resourceName',
message: 'Please select the API you would want to update',
type: 'list',
choices: resources,
},
];
const { resourceName } = await inquirer.prompt(question);
const resourceSettings = allResources.find(
(resource) =>
resource.resourceName === resourceName &&
resource.category === category &&
resource.service === serviceName &&
!!resource.providerPlugin,
);
let { gitHubInfo: { path = undefined } = {} } = resourceSettings;
let gitHubToken;
if (resourceSettings.deploymentMechanism === DEPLOYMENT_MECHANISM.INDENPENDENTLY_MANAGED) {
if (await confirm('Would you like to change your GitHub access token')) {
const gitHubQuestion = await inquirer.prompt({
name: 'gitHubAccessToken',
type: 'password',
message: 'GitHub Personal Access Token:',
});
gitHubToken = gitHubQuestion.gitHubAccessToken;
}
if (await confirm('Would you like to change your GitHub Path to your repo')) {
const gitHubQuestion = await inquirer.prompt({
name: 'gitHubPath',
type: 'input',
message: 'Path to your repo:',
default: path,
});
path = gitHubQuestion.gitHubPath;
}
}
const { environmentMap = {}, mutableParametersState = {} } = resourceSettings;
const meta = context.amplify.getProjectDetails().amplifyMeta;
const hasAccessableResources = ['storage', 'function'].some((categoryName) => {
return Object.keys(meta[categoryName] ?? {}).length > 0;
});
let rolePermissions: any = {};
if (
hasAccessableResources &&
(await context.amplify.confirmPrompt('Do you want to access other resources in this project from your api?'))
) {
rolePermissions = await context.amplify.invokePluginMethod(context, 'function', undefined, 'askExecRolePermissionsQuestions', [
context,
resourceName,
mutableParametersState.permissions,
environmentMap,
category,
serviceName,
]);
}
const {
categoryPolicies = [],
environmentMap: newEnvironmentMap,
dependsOn: newDependsOn = [],
mutableParametersState: newMutableParametersState,
} = rolePermissions;
const { restrict_access: restrictAccess } = await inquirer.prompt({
name: 'restrict_access',
type: 'confirm',
message: 'Do you want to restrict API access',
default: resourceSettings.restrictAccess,
});
return {
...resourceSettings,
restrictAccess,
environmentMap: newEnvironmentMap,
mutableParametersState: newMutableParametersState,
dependsOn: newDependsOn,
categoryPolicies,
gitHubPath: path,
gitHubToken,
};
}
async function confirm(question: string) {
const { confirm } = await inquirer.prompt({
type: 'confirm',
default: false,
message: question,
name: 'confirm',
});
return confirm;
}
export async function getPermissionPolicies(context, service, resourceName, crudOptions) {
throw new Error('IAM access not available for this resource');
}