UNPKG

@aws-amplify/cli-internal

Version:
412 lines • 21.4 kB
"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