@aws-amplify/cli-internal
Version:
Amplify CLI
412 lines • 21.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FunctionGenerator = void 0;
const node_path_1 = __importDefault(require("node:path"));
const promises_1 = __importDefault(require("node:fs/promises"));
const amplify_cli_core_1 = require("@aws-amplify/amplify-cli-core");
const ts_1 = require("../../ts");
const function_renderer_1 = require("./function.renderer");
const kinesis_generator_1 = require("../analytics/kinesis.generator");
const auth_mappings_1 = require("./auth-mappings");
class FunctionGenerator {
constructor(options) {
this.gen1App = options.gen1App;
this.backendGenerator = options.backendGenerator;
this.packageJsonGenerator = options.packageJsonGenerator;
this.outputDir = options.outputDir;
this.resource = options.resource;
this.renderer = new function_renderer_1.FunctionRenderer(options.gen1App.appId, options.gen1App.envName);
this.logger = options.logger;
}
setAuthGenerator(authGenerator) {
this.authGenerator = authGenerator;
}
setS3Generator(s3Generator) {
this.s3Generator = s3Generator;
}
async plan() {
var _a, _b, _c;
const resourceName = this.resource.resourceName;
const deployedName = this.gen1App.resourceMetaOutput(this.resource, 'Name');
this.logger.debug(`Fetching Lambda function config '${deployedName}'`);
const config = await this.gen1App.aws.fetchFunctionConfig(deployedName);
if (!config)
throw new amplify_cli_core_1.AmplifyError('LambdaFunctionNotFoundError', {
message: `Lambda function '${deployedName}' not found`,
resolution: 'Verify the Lambda function exists and the CLI has the correct AWS credentials and region configured.',
});
const runtime = config.Runtime;
if (runtime && !runtime.startsWith('nodejs')) {
throw new amplify_cli_core_1.AmplifyError('UnsupportedRuntimeError', {
message: `Function '${deployedName}' uses unsupported runtime '${runtime}'. Gen 2 migration only supports Node.js functions.`,
resolution: 'Migrate the function to a Node.js runtime before running the Gen 2 migration.',
});
}
this.logger.debug(`Fetching Lambda function schedule '${deployedName}'`);
const schedule = await this.gen1App.aws.fetchFunctionSchedule(deployedName);
const entry = ts_1.TS.extractFilePathFromHandler((_a = config.Handler) !== null && _a !== void 0 ? _a : 'index.js');
const { literalEnvVars, dynamicEnvVars } = (0, function_renderer_1.classifyEnvVars)((_c = (_b = config.Environment) === null || _b === void 0 ? void 0 : _b.Variables) !== null && _c !== void 0 ? _c : {});
const dynamoActions = this.extractDynamoActions();
const kinesisActions = this.extractKinesisActions();
const appSyncPermissions = this.extractAppSyncPermissions();
const { authAccess, unMappedAuthActions } = this.extractAuthPermissions();
const dataTriggerModels = this.detectDataTriggerModels();
const storageTriggerTables = this.detectDynamoTriggerTables();
const isKinesisTrigger = this.isKinesisTrigger();
const hasAnalytics = kinesisActions.length > 0 || isKinesisTrigger;
const renderOpts = {
resourceName,
entry,
name: deployedName,
timeoutSeconds: config.Timeout,
memoryMB: config.MemorySize,
runtime,
schedule,
literalEnvVars,
dynamicEnvVars,
dynamoActions,
appSyncPermissions,
dataTriggerModels,
storageTriggerTables,
unMappedAuthActions,
kinesisConfig: hasAnalytics
? {
resourceName: this.gen1App.singleResourceName('analytics', 'Kinesis'),
actions: kinesisActions,
isTrigger: isKinesisTrigger,
}
: undefined,
};
this.contributeDependencies();
this.contributeAuthAccess(authAccess);
this.contributeAuthTrigger();
this.contributeStorageAccess();
this.contributeStorageTrigger();
return [
{
resource: this.resource,
validate: () => undefined,
describe: async () => [`Generate amplify/function/${resourceName}/resource.ts`],
execute: async () => {
const dirPath = node_path_1.default.join(this.outputDir, 'amplify', 'function', resourceName);
this.logger.info(`Rendering function/${resourceName}/resource.ts`);
const nodes = this.renderer.render(renderOpts);
const content = ts_1.TS.printNodes(nodes);
await promises_1.default.mkdir(dirPath, { recursive: true });
await promises_1.default.writeFile(node_path_1.default.join(dirPath, 'resource.ts'), content, 'utf-8');
await this.copyFunctionSource(resourceName, dirPath);
const alias = resourceName;
this.backendGenerator.addNamespaceImport(alias, `./function/${resourceName}/resource`);
this.backendGenerator.addDefineBackendEntry(resourceName, alias, resourceName);
const escapeHatchArgs = this.deriveApplyEscapeHatchArguments(hasAnalytics, dynamicEnvVars);
this.backendGenerator.addApplyEscapeHatchesCall({ alias, extraArgs: escapeHatchArgs });
},
},
];
}
contributeAuthAccess(authAccess) {
if (!this.authGenerator)
return;
if (Object.keys(authAccess).length > 0) {
this.authGenerator.addFunctionAuthAccess({ resourceName: this.resource.resourceName, permissions: authAccess });
}
}
contributeAuthTrigger() {
if (!this.authGenerator)
return;
const authResourceName = this.gen1App.singleResourceName('auth', 'Cognito');
if (!this.resource.resourceName.startsWith(authResourceName))
return;
const suffix = this.resource.resourceName.slice(authResourceName.length);
const event = auth_mappings_1.AUTH_TRIGGER_SUFFIX_TO_EVENT[suffix];
if (event)
this.authGenerator.addTrigger({ event, resourceName: this.resource.resourceName });
}
contributeStorageAccess() {
var _a, _b, _c, _d;
if (!this.s3Generator)
return;
const S3_ACTION_TO_PERMISSION = {
's3:GetObject': 'read',
's3:PutObject': 'write',
's3:DeleteObject': 'delete',
's3:ListBucket': 'read',
};
const templatePath = `function/${this.resource.resourceName}/${this.resource.resourceName}-cloudformation-template.json`;
const template = this.gen1App.json(templatePath);
const policy = (_a = template.Resources) === null || _a === void 0 ? void 0 : _a.AmplifyResourcesPolicy;
if (!policy || policy.Type !== 'AWS::IAM::Policy')
return;
const statements = (_d = (_c = (_b = policy.Properties) === null || _b === void 0 ? void 0 : _b.PolicyDocument) === null || _c === void 0 ? void 0 : _c.Statement) !== null && _d !== void 0 ? _d : [];
const permissions = new Set();
for (const statement of Array.isArray(statements) ? statements : [statements]) {
if (statement.Effect !== 'Allow')
continue;
const actions = Array.isArray(statement.Action) ? statement.Action : [statement.Action];
for (const action of actions) {
if (typeof action === 'string' && S3_ACTION_TO_PERMISSION[action])
permissions.add(S3_ACTION_TO_PERMISSION[action]);
}
}
if (permissions.size > 0)
this.s3Generator.addFunctionAccess(this.resource.resourceName, Array.from(permissions));
}
contributeStorageTrigger() {
var _a, _b, _c, _d, _e, _f;
if (!this.s3Generator)
return;
const storageCategory = this.gen1App.categoryMeta('storage');
if (!storageCategory)
return;
const s3Entry = Object.entries(storageCategory).find(([, v]) => v.service === 'S3');
if (!s3Entry)
return;
const [storageName] = s3Entry;
const templatePath = `storage/${storageName}/build/cloudformation-template.json`;
const template = this.gen1App.json(templatePath);
const lambdaConfigs = (_e = (_d = (_c = (_b = (_a = template === null || template === void 0 ? void 0 : template.Resources) === null || _a === void 0 ? void 0 : _a.S3Bucket) === null || _b === void 0 ? void 0 : _b.Properties) === null || _c === void 0 ? void 0 : _c.NotificationConfiguration) === null || _d === void 0 ? void 0 : _d.LambdaConfigurations) !== null && _e !== void 0 ? _e : [];
for (const config of lambdaConfigs) {
const functionRef = (_f = config === null || config === void 0 ? void 0 : config.Function) === null || _f === void 0 ? void 0 : _f.Ref;
if (!functionRef || !functionRef.includes(this.resource.resourceName))
continue;
const event = config.Event;
if (event === null || event === void 0 ? void 0 : event.includes('ObjectCreated'))
this.s3Generator.addTrigger('onUpload', this.resource.resourceName);
else if (event === null || event === void 0 ? void 0 : event.includes('ObjectRemoved'))
this.s3Generator.addTrigger('onDelete', this.resource.resourceName);
}
}
async copyFunctionSource(resourceName, destDir) {
const srcDir = node_path_1.default.join('amplify', 'backend', 'function', resourceName, 'src');
await promises_1.default.cp(srcDir, destDir, {
recursive: true,
filter: (src) => {
const b = node_path_1.default.basename(src);
return (b !== 'node_modules' &&
b !== '.yarn' &&
b !== 'package.json' &&
b !== 'package-lock.json' &&
b !== 'yarn.lock' &&
b !== 'pnpm-lock.yaml');
},
});
}
contributeDependencies() {
const packageJsonPath = node_path_1.default.join('amplify', 'backend', 'function', this.resource.resourceName, 'src', 'package.json');
const pkg = amplify_cli_core_1.JSONUtilities.readJson(packageJsonPath, { throwIfNotExist: false });
if (pkg === null || pkg === void 0 ? void 0 : pkg.dependencies) {
for (const [n, v] of Object.entries(pkg.dependencies))
this.packageJsonGenerator.addDependency(n, v);
}
if (pkg === null || pkg === void 0 ? void 0 : pkg.devDependencies) {
for (const [n, v] of Object.entries(pkg.devDependencies))
this.packageJsonGenerator.addDevDependency(n, v);
}
}
deriveApplyEscapeHatchArguments(hasAnalytics, dynamicEnvVars) {
const args = [];
for (const dynamicEnvVar of dynamicEnvVars) {
if (!dynamicEnvVar.name.startsWith('STORAGE_') || dynamicEnvVar.name.endsWith('BUCKETNAME'))
continue;
const match = dynamicEnvVar.name.match(/STORAGE_(.+?)_(ARN|NAME|STREAMARN)$/);
if (match)
args.push(match[1].toLowerCase());
}
if (hasAnalytics) {
args.push(kinesis_generator_1.DEFINE_ANALYTICS_VARIABLE_NAME);
}
return Array.from(new Set(args));
}
detectDataTriggerModels() {
var _a;
const templatePath = `function/${this.resource.resourceName}/${this.resource.resourceName}-cloudformation-template.json`;
const template = this.gen1App.json(templatePath);
const models = [];
for (const resource of Object.values((_a = template.Resources) !== null && _a !== void 0 ? _a : {})) {
const res = resource;
if (res.Type !== 'AWS::Lambda::EventSourceMapping')
continue;
const props = res.Properties;
const eventSourceArn = props === null || props === void 0 ? void 0 : props.EventSourceArn;
const fnImportValue = eventSourceArn === null || eventSourceArn === void 0 ? void 0 : eventSourceArn['Fn::ImportValue'];
const fnSub = fnImportValue === null || fnImportValue === void 0 ? void 0 : fnImportValue['Fn::Sub'];
if (!fnSub)
continue;
const match = fnSub.match(/:GetAtt:(\w+)Table:StreamArn/);
if (match)
models.push(match[1]);
}
return models;
}
detectDynamoTriggerTables() {
const templatePath = `function/${this.resource.resourceName}/${this.resource.resourceName}-cloudformation-template.json`;
const template = this.gen1App.json(templatePath);
const tables = [];
for (const resource of Object.values(template.Resources)) {
const res = resource;
if (res.Type !== 'AWS::Lambda::EventSourceMapping')
continue;
const props = res.Properties;
const eventSourceArn = props.EventSourceArn;
if (!('Ref' in eventSourceArn))
continue;
const match = eventSourceArn.Ref.match(/^storage(\w+)StreamArn$/);
if (match) {
tables.push(match[1]);
}
}
return tables;
}
isKinesisTrigger() {
var _a;
const templatePath = `function/${this.resource.resourceName}/${this.resource.resourceName}-cloudformation-template.json`;
const template = this.gen1App.json(templatePath);
for (const resource of Object.values((_a = template.Resources) !== null && _a !== void 0 ? _a : {})) {
const res = resource;
if (res.Type !== 'AWS::Lambda::EventSourceMapping')
continue;
const props = res.Properties;
const eventSourceArn = props === null || props === void 0 ? void 0 : props.EventSourceArn;
if (!eventSourceArn || !('Ref' in eventSourceArn))
continue;
if (/^analytics\w+kinesisStreamArn$/.test(eventSourceArn.Ref))
return true;
}
return false;
}
readPolicyStatements() {
var _a, _b, _c, _d;
const templatePath = `function/${this.resource.resourceName}/${this.resource.resourceName}-cloudformation-template.json`;
const template = this.gen1App.json(templatePath);
const policy = (_a = template.Resources) === null || _a === void 0 ? void 0 : _a.AmplifyResourcesPolicy;
if (!policy || policy.Type !== 'AWS::IAM::Policy')
return [];
return (_d = (_c = (_b = policy.Properties) === null || _b === void 0 ? void 0 : _b.PolicyDocument) === null || _c === void 0 ? void 0 : _c.Statement) !== null && _d !== void 0 ? _d : [];
}
extractDynamoActions() {
const actions = [];
for (const statement of this.readPolicyStatements()) {
const statementActions = Array.isArray(statement.Action) ? statement.Action : [statement.Action];
for (const action of statementActions) {
if (typeof action === 'string' && action.startsWith('dynamodb:'))
actions.push(action);
}
}
return actions;
}
extractKinesisActions() {
const actions = [];
for (const statement of this.readPolicyStatements()) {
const statementActions = Array.isArray(statement.Action) ? statement.Action : [statement.Action];
for (const action of statementActions) {
if (typeof action === 'string' && action.startsWith('kinesis:') && !actions.includes(action))
actions.push(action);
}
}
return actions;
}
extractAppSyncPermissions() {
let hasMutation = false;
let hasQuery = false;
for (const statement of this.readPolicyStatements()) {
const resources = Array.isArray(statement.Resource) ? statement.Resource : [statement.Resource];
for (const resource of resources) {
const resourceStr = JSON.stringify(resource);
if (resourceStr.includes('/types/Mutation/'))
hasMutation = true;
if (resourceStr.includes('/types/Query/'))
hasQuery = true;
}
}
return { hasMutation, hasQuery };
}
extractAuthPermissions() {
var _a, _b, _c, _d;
const cognitoActions = [];
const otherAuthActions = [];
for (const statement of this.readPolicyStatements()) {
const statementActions = Array.isArray(statement.Action) ? statement.Action : [statement.Action];
const statementResources = Array.isArray(statement.Resource) ? statement.Resource : [statement.Resource];
const targetsUserPool = JSON.stringify(statementResources).includes('userpool/') || JSON.stringify(statementResources).includes('UserPool');
for (const action of statementActions) {
if (typeof action !== 'string')
continue;
if (action.startsWith('cognito-idp:')) {
if (action === 'cognito-idp:AdminList*') {
for (const a of ['cognito-idp:AdminListDevices', 'cognito-idp:AdminListGroupsForUser']) {
if (!cognitoActions.includes(a))
cognitoActions.push(a);
}
}
else if (action === 'cognito-idp:List*') {
for (const a of ['cognito-idp:ListUsers', 'cognito-idp:ListUsersInGroup', 'cognito-idp:ListGroups']) {
if (!cognitoActions.includes(a))
cognitoActions.push(a);
}
}
else if (!cognitoActions.includes(action)) {
cognitoActions.push(action);
}
}
else if (targetsUserPool && !otherAuthActions.includes(action)) {
otherAuthActions.push(action);
}
}
}
const authCategory = this.gen1App.categoryMeta('auth');
if (authCategory) {
const authResourceName = this.gen1App.singleResourceName('auth', 'Cognito');
const templatePath = `auth/${authResourceName}/build/auth-trigger-cloudformation-template.json`;
if (this.gen1App.fileExists(templatePath)) {
const template = this.gen1App.json(templatePath);
const resources = (_a = template.Resources) !== null && _a !== void 0 ? _a : {};
for (const [logicalId, resource] of Object.entries(resources)) {
const res = resource;
if (res.Type !== 'AWS::IAM::Policy')
continue;
if (!logicalId.includes(this.resource.resourceName))
continue;
const statements = (_d = (_c = (_b = res.Properties) === null || _b === void 0 ? void 0 : _b.PolicyDocument) === null || _c === void 0 ? void 0 : _c.Statement) !== null && _d !== void 0 ? _d : [];
for (const statement of statements) {
const actions = Array.isArray(statement.Action) ? statement.Action : [statement.Action];
for (const action of actions) {
if (typeof action === 'string' && action.startsWith('cognito-idp:') && !cognitoActions.includes(action)) {
cognitoActions.push(action);
}
}
}
}
}
}
const { permissions: authAccess, unMapped } = resolveAuthAccess(cognitoActions);
return { authAccess, unMappedAuthActions: [...unMapped, ...otherAuthActions] };
}
}
exports.FunctionGenerator = FunctionGenerator;
function resolveAuthAccess(cognitoActions) {
if (cognitoActions.length === 0)
return { permissions: {}, unMapped: [] };
const result = {};
const covered = new Set();
for (const [group, required] of Object.entries(auth_mappings_1.GROUPED_AUTH_PERMISSIONS)) {
if (required.every((a) => cognitoActions.includes(a))) {
result[group] = true;
for (const a of required)
covered.add(a);
}
}
for (const action of cognitoActions) {
if (covered.has(action))
continue;
if (auth_mappings_1.SINGULAR_AUTH_PERMISSIONS[action]) {
result[auth_mappings_1.SINGULAR_AUTH_PERMISSIONS[action]] = true;
covered.add(action);
}
}
const unMapped = cognitoActions.filter((a) => !covered.has(a));
return { permissions: result, unMapped: unMapped };
}
//# sourceMappingURL=function.generator.js.map