UNPKG

serverless-leo

Version:
1,042 lines (960 loc) 36.1 kB
/* eslint-disable space-before-function-paren */ /* eslint-disable no-template-curly-in-string */ const fs = require('fs') const path = require('path') const { fetchAll } = require('./utils') // Try to get serverless 3 version, if that fails try serverless 2 version // let resolveCfRefValue; // try { // resolveCfRefValue = require('serverless/lib/plugins/aws/utils/resolve-cf-ref-value'); // } catch (err) { // resolveCfRefValue = require('serverless/lib/plugins/aws/utils/resolveCfRefValue'); // } async function resolveCfRefValue(provider, resourceLogicalId, sdkParams = {}) { let params = (provider.serverless.service.resources.Parameters || {}) if (resourceLogicalId in params) { return (provider.serverless.service.provider.stackParameters || {})[resourceLogicalId] } return provider .request( 'CloudFormation', 'listStackResources', Object.assign({ StackName: provider.naming.getStackName() }, sdkParams) ) .then((result) => { const targetStackResource = result.StackResourceSummaries.find( (stackResource) => stackResource.LogicalResourceId === resourceLogicalId ) if (targetStackResource) return targetStackResource.PhysicalResourceId if (result.NextToken) { return resolveCfRefValue(provider, resourceLogicalId, { NextToken: result.NextToken }) } throw new Error( `Could not resolve Ref with name ${resourceLogicalId}. Are you sure this value matches one resource logical ID ?`, 'CF_REF_RESOLUTION' ) }) } let ts let paths = module.paths try { module.paths = require('module')._nodeModulePaths(process.cwd()) ts = require('typescript') } catch (e) { // No Typescript } finally { module.paths = paths } const isTS = ts != null function generateConfig(filePath, serverless) { let ext = isTS ? '.ts' : '.js' let dTSFilePath = filePath.replace(/\.[^\/]*\.json$/, '.d.ts') let configOutputPath = filePath.replace(/\.[^\/]*\.json$/, ext) let projectConfigTxt = fs.existsSync(filePath) && fs.readFileSync(filePath).toString() if (!projectConfigTxt) { return } let projectConfig = JSON.parse(projectConfigTxt) let buildConfig = projectConfig.configBuilderOptions || {} delete projectConfig.configBuilderOptions let interfaces = {} if (isTS && fs.existsSync(dTSFilePath)) { let src = ts.createSourceFile('blah.ts', fs.readFileSync(dTSFilePath).toString(), ts.ScriptTarget.ES2022) src.forEachChild(child => { if (child.kind === ts.SyntaxKind.InterfaceDeclaration) { let o = build(child, projectConfig, []) let intf = child let flat = {} flattenVariables(o, flat, '.', '') interfaces[intf.name.escapedText] = flat } }) } let interfaceName = Object.keys(interfaces)[0] let configInterface = interfaces[interfaceName] || {} function expandConfig(projectConfig, path) { if (projectConfig == null || typeof projectConfig !== 'object') { return projectConfig } let o = {} Object.entries(projectConfig).forEach(([key, value]) => { let fieldPath = path.concat(key) if (typeof value === 'string' && value.match(/^.+?::/)) { let [service, key, type, opts] = value.split(/(?<!AWS)::/) value = { is_config_reference: true, service, key, type: (configInterface[fieldPath.join('.')] || {}).type || type || 'dynamic', options: opts && inferTypes((opts).split(';').reduce((all, one) => { let [key, value] = one.split('=') if (key !== '') { all[key] = value == null ? true : value } return all }, {})) } } if ( value != null && typeof value === 'object' && (value.service == null || value.key == null || value.type == null) ) { if (Array.isArray(value)) { value = value.map((a, i) => expandConfig(a, fieldPath.concat(i))) } else { value = expandConfig(value, fieldPath) } } o[key] = value }) return o } let eConfig = expandConfig(projectConfig, []) let interfaceDef = '' let all = (projectConfigTxt.match(/^(\t| )/gm) || []).reduce((s, o) => { if (o === '\t') { s.tabs++ } else { s.spaces++ } return s }, { tabs: 0, spaces: 0 }) let spaces = '\t' if (all.spaces > all.tabs) { spaces = (projectConfigTxt.match(/^( +)/gm) || [' '])[0] } if (interfaceName == null) { interfaceName = toProperCase(path.basename(configOutputPath).replace(/\.[tj]s$/, '')) } let spacesLength = spaces.length let imports = new Set() let knownTypes = { 'string': 'string', 'number': 'number', 'float': 'number', 'integer': 'number', 'int': 'number', 'dynamic': 'dynamic', 'unknown': 'unknown', 'undefined': 'undefined' } function getType(field, depth = '') { if (field != null && typeof field === 'object') { if (field.service != null && field.key != null && field.type != null) { let t = field.type === 'dynamic' ? 'unknown' : field.type let collection = 'TYPE' let matchParts // eslint-disable-next-line no-cond-assign if (matchParts = t.match(/\[\]$/)) { t = t.replace(/\[\]$/, '') collection = 'TYPE[]' // eslint-disable-next-line no-cond-assign } else if (matchParts = t.match(/(Map|Set|Array)<(.*?)>/)) { collection = `${matchParts[1]}<TYPE>` t = matchParts[2].split(',').map(t => t.trim()) } if (!Array.isArray(t)) { t = [t] } // t = t.reduce((a, b) => a.concat(b.split('|')), []) t = t.map(t => { return t.split('|').map(t => { if (!knownTypes[t]) { if (!t.startsWith('{')) { imports.add(t) } } else { t = knownTypes[t] } return t }).join('|') }).join(', ') return collection.replace('TYPE', t) } else { let nextDepth = depth += spaces if (Array.isArray(field)) { // Get unique set of types in the array let r = Array.from(new Set(Object.entries(field).map(([key, value]) => { return getType(value, nextDepth) }))) // Join them togethere if there is more than 1 let rLen = r.length r = r.join('|') if (rLen > 1) { r = `(${r})` } // return array type return `${r}[]` } else { let r = Object.entries(field).map(([key, value]) => { // console.log(value) // eslint-disable-next-line eqeqeq const question = value.options && value.options.optional == true ? '?' : '' return `${nextDepth}${key}${question}: ${getType(value, nextDepth)};` }).join('\n') return `{\n${r}\n${depth.substring(0, depth.length - spacesLength)}}` } } } else { return typeof field } } interfaceDef = resolveKeywords( '${imports}export interface ${interfaceName} ${value}\n', { interfaceName: interfaceName, value: getType(eConfig), imports: imports.size ? `import { ${Array.from(imports).join(', ')} } from "${((serverless.service.custom || {}).leo || {}).rsfTypesModule || 'types'}";\n\n` : '' }, { spaces: spaces }) let template = [ '/* Generated by serverless-leo */', isTS ? 'import { ConfigurationBuilder } from "leo-sdk/lib/configuration-builder";' : 'const { ConfigurationBuilder } = require("leo-sdk/lib/configuration-builder");', isTS ? '${interfaceDef}' : '', // (isTS ? 'export default' : 'module.exports = ') + ' new ConfigurationBuilder' + (isTS ? '<${interfaceName}>' : '') + '(${config}).build(${buildConfig});' (isTS ? 'export default' : 'module.exports = ') + ' new ConfigurationBuilder' + (isTS ? '<${interfaceName}>' : '') + '(process.env.RSF_CONFIG ?? "").build(${buildConfig});' ].join('\n') let fileBody = resolveKeywords(template, { interfaceDef: interfaceDef, interfaceName: interfaceName, config: JSON.stringify(eConfig, (_key, value) => { if (value != null && typeof value === 'object' && value.service && value.key && value.type) { return `__CLASS_START__new ${toProperCase(value.service)}Resource("${value.key}", "${value.type}"${value.options != null ? (', ' + JSON.stringify(value.options)) : ''}),__CLASS_END__` } return value }, spaces).replace(/"__CLASS_START__(.*?)__CLASS_END__",?/g, (_a, b) => { return b.replace(/\\"/g, '"') }), buildConfig: buildConfig }, { spaces: spaces }) fs.writeFileSync(configOutputPath, fileBody) if (!isTS) { fs.writeFileSync(dTSFilePath, [ `${interfaceDef}`, `declare const configuration: ${interfaceName};`, `export default configuration;` ].join('\n')) } return eConfig } function build(child, projectConfig, path) { let o = {}; (child.members || []).forEach((field) => { let type = ts.SyntaxKind[field.type.kind].replace(/Keyword/, '').toLowerCase() let name = field.name.escapedText let fieldPath = path.concat(name) let value = { _isLeaf: true, type, name, path: fieldPath.join('.'), projectConfigValue: projectConfig[name] } if (field.type.members != null) { value = build(field.type, projectConfig[name], fieldPath) } o[name] = value }) return o } function flattenVariables(obj, out, separator, prefix) { prefix = prefix || '' separator = separator || ':' Object.keys(obj).forEach((k) => { var v = obj[k] if (typeof v === 'object' && !(Array.isArray(v)) && v !== null && !v._isLeaf) { flattenVariables(v, out, separator, prefix + k + separator) } else { out[prefix + k] = v } }) } function toProperCase(text) { return text.replace(/[^a-zA-Z0-9]+/g, '_').replace(/(^\w|_\w)/g, function(txt) { return txt.charAt(txt.length === 1 ? 0 : 1).toUpperCase() }) } function getDataSafe(data = {}, path = '') { const pathArray = path.split('.').filter(a => a !== '') if (pathArray.length === 0) { return data } const lastField = pathArray.pop() return pathArray.reduce((parent, field) => parent[field] || {}, data)[lastField] } function resolveKeywords(template, data, opts) { const name = template.replace(/\${(.*?)}/g, function(match, field) { let value = getDataSafe(data, field.trim()) if (value != null && typeof value === 'object') { value = JSON.stringify(value, null, opts.spaces || 2) } return value != null ? value : match }).replace(/[_-]{2,}/g, '') return name } const numberRegex = /^\d+(?:\.\d*)?$/ const boolRegex = /^(?:false|true)$/i const nullRegex = /^null$/ const undefinedRegex = /^undefined$/ const jsonRegex = /^{(.|\n)*}$/ function inferTypes(node) { let type = typeof node if (Array.isArray(node)) { for (let i = 0; i < node.length; i++) { node[i] = inferTypes(node[i]) } } else if (type === 'object' && node !== null) { Object.keys(node).map(key => { node[key] = inferTypes(node[key]) }) } else if (type === 'string') { if (numberRegex.test(node)) { return parseFloat(node) } else if (boolRegex.test(node)) { return node.toLowerCase() === 'true' } else if (nullRegex.test(node)) { return null } else if (undefinedRegex.test(node)) { return undefined } else if (jsonRegex.test(node)) { return JSON.parse(node) } } return node } function getConfigFullPath(serverless, file) { if (file) { file = path.resolve(process.cwd(), file) } else if (serverless.service.custom && serverless.service.custom.leo && serverless.service.custom.leo.configurationPath) { file = path.resolve(serverless.serviceDir || serverless.servicePath, serverless.service.custom.leo.configurationPath) } else { file = path.resolve(process.cwd(), './project-config.def.json') } return file } function getConfigReferences(config, useSecretsManager, lookups = [], permissions = new Set()) { let output = {} Object.entries(config || {}).forEach(([key, value]) => { if (value != null && typeof value === 'object' && value.service != null && value.key != null && value.type != null) { if (typeof value.key === 'string') { value.key = value.key.replace(/\$\{region\}/gi, '${AWS::Region}').replace(/\$\{accountId\}/gi, '${AWS::AccountId}') } let v switch (value.service) { case 'cf': v = { 'Fn::ImportValue': { 'Fn::Sub': value.key } }; break case 'cfr': v = { 'Ref': value.key.replace(/\./g, '').replace(/\$\{.*?\}/g, '') }; break case 'ssm': v = { 'Fn::Sub': `{{resolve:ssm:${value.key}}}` }; break case 'secret': if ((value.options && value.options.resolve === 'runtime')) { v = { 'Fn::Sub': `secret::${value.key}::${value.type}` } let secretKey = value.key.split('.')[0] permissions.add(`arn:aws:secretsmanager:*:\$\{AWS::AccountId\}:secret:${secretKey}-*`) } else { let parts = value.key.split('.') parts = [parts[0], 'SecretString'].concat(parts.splice(1).join('.')) if (parts[parts.length - 1] === '') { parts.pop() } v = { 'Fn::Sub': `{{resolve:secretsmanager:${parts.join(':')}}}` } } break case 'stack': v = { [value.key.match(/\$\{/) ? 'Fn::Sub' : 'Ref']: value.key }; break case 'static': v = undefined output[key] = value.key return } output[key] = `\${RSF${lookups.length}}` lookups.push(v) } else if (value != null && Array.isArray(value)) { let r = value.map(v => { if (v == null || typeof (v) !== 'object') { return v } return getConfigReferences(v, useSecretsManager, lookups, permissions).output }) output[key] = r } else if (value != null && typeof value === 'object') { let r = getConfigReferences(value, useSecretsManager, lookups, permissions) output[key] = r.output } else { output[key] = value } }) return { output, lookups: lookups.reduce((all, one, index) => { all[`RSF${index}`] = one return all }, {}), permissions } } async function resolveConfigForLocal(serverless, cache = {}) { let rsfConfigEnvTemplate = serverless.service.provider.environment && serverless.service.provider.environment.RSF_CONFIG let fullStage = `${serverless.providers.aws.getRegion()}-${serverless.service.provider.environment.RSF_INVOKE_STAGE}` let configFromCache = false let serviceDir = serverless.config.serviceDir || serverless.config.servicePath let configFileCache = path.resolve(serviceDir, `.rsf/config-${fullStage}.json`) if (fs.existsSync(configFileCache)) { let stat = fs.statSync(configFileCache) let duration = Math.floor((Date.now() - stat.mtimeMs) / 1000) // Default cache duration is 30 min let validCacheDuration = (+process.env.RSF_CACHE_SECONDS) || 1800 if (duration < validCacheDuration) { let prevTemplate try { prevTemplate = fs.readFileSync(path.resolve(serviceDir, `.rsf/config-${fullStage}-template.json`)).toString() } catch (e) { // nothing } try { let stringifyedTemplate = JSON.stringify(rsfConfigEnvTemplate, null, 2) if (prevTemplate === stringifyedTemplate) { serverless.service.provider.environment.RSF_CONFIG = JSON.stringify(module.require(configFileCache)) configFromCache = true } else { // Remove the cached config fs.unlinkSync(configFileCache) fs.writeFileSync(path.resolve(serviceDir, `.rsf/config-${fullStage}-template.json`), stringifyedTemplate) } } catch (e) { // Error getting cache } } } cache = { stack: {}, cf: {}, sm: {}, ssm: {}, cfr: {}, ...cache } // Resolve config env var if (!configFromCache && rsfConfigEnvTemplate) { let v = { value: rsfConfigEnvTemplate } await resolveTemplate(v, serverless, cache) serverless.service.provider.environment.RSF_CONFIG = v.value } let busConfigFromCache = false let busConfigFileCache = path.resolve(serviceDir, `.rsf/bus-config-${fullStage}.json`) if (fs.existsSync(busConfigFileCache)) { let stat = fs.statSync(busConfigFileCache) let duration = Math.floor((Date.now() - stat.mtimeMs) / 1000) // Default cache duration is 30 min let validCacheDuration = (+process.env.RSF_CACHE_SECONDS) || 1800 if (duration < validCacheDuration) { try { serverless.service.provider.environment.RSTREAMS_CONFIG = JSON.stringify(module.require(busConfigFileCache)) busConfigFromCache = true } catch (e) { // Error getting cache } } } // Resolve bus env config if it exists let rstreamsConfigEnvTemplate = serverless.service.provider.environment && serverless.service.provider.environment.RSTREAMS_CONFIG if (!busConfigFromCache && rstreamsConfigEnvTemplate) { let v = { value: rstreamsConfigEnvTemplate } await resolveTemplate(v, serverless, cache) serverless.service.provider.environment.RSTREAMS_CONFIG = v.value fs.mkdirSync(path.dirname(busConfigFileCache), { recursive: true }) fs.writeFileSync(busConfigFileCache, JSON.stringify(JSON.parse(v.value), null, 2)) } // Resolve bus env secret if it exists let rstreamsConfigEnvSecretTemplate = serverless.service.provider.environment && serverless.service.provider.environment.RSTREAMS_CONFIG_SECRET if (rstreamsConfigEnvSecretTemplate) { let v = { value: rstreamsConfigEnvSecretTemplate } await resolveTemplate(v, serverless, cache) serverless.service.provider.environment.RSTREAMS_CONFIG_SECRET = v.value } let env = serverless.service.provider.environment if (env) { let v = { value: env } await resolveTemplate(v, serverless, cache) serverless.service.provider.environment = v.value } } const resolveServices = { secretsmanager: async (provider, key, cache) => { let parts = key.split(':') let id = parts.shift() let last = parts.pop() let value if (id in cache.sm) { value = cache.sm[id] } else { value = await provider.request('SecretsManager', 'getSecretValue', { SecretId: id }) cache.sm[id] = value } return parts.reduce((o, k) => { let v = o[k] || {} if (typeof v === 'string') { v = JSON.parse(v) } return v }, value)[last] }, ssm: async (provider, key, cache) => { if (key in cache.ssm) { return cache.ssm[key] } else { let value = await provider.request('SSM', 'getParameter', { Name: key }) cache.ssm[key] = value.Parameter.Value return value.Parameter.Value } }, cf: async (provider, key, cache, serverless) => { if (Object.keys(cache.cf).length === 0) { let serviceDir = serverless.config.serviceDir || serverless.config.servicePath let fullStage = `${serverless.providers.aws.getRegion()}-${serverless.service.provider.stage}` let file = path.resolve(serviceDir, `.rsf/cf-exports-${fullStage}.json`) let allCfExports if (fs.existsSync(file)) { let stat = fs.statSync(file) let duration = Math.floor((Date.now() - stat.mtimeMs) / 1000) // Default cache duration is 30 min let validCacheDuration = (+process.env.RSF_CACHE_SECONDS) || 1800 if (duration < validCacheDuration) { allCfExports = JSON.parse(fs.readFileSync(path.resolve(serviceDir, file)).toString()) } } if (allCfExports == null) { allCfExports = await fetchAll(t => provider.request('CloudFormation', 'listExports', { NextToken: t })) fs.writeFileSync(file, JSON.stringify(allCfExports)) } allCfExports.Exports.forEach(v => { cache.cf[v.Name] = v.Value }) } return cache.cf[key] }, cfr: async (provider, key, cache) => { if (!(key in cache.cfr)) { let [stack, resource] = key.split('.') cache.cfr[key] = await resolveCfRefValue(provider, resource, { StackName: stack }) } return cache.cfr[key] }, stack: async (provider, key, cache) => { if (!(key in cache.stack)) { cache.stack[key] = await resolveCfRefValue(provider, key) } return cache.stack[key] } } async function resolveTemplate(template, serverless, cache, lookups) { if (lookups == null) { lookups = await getLookups(serverless, cache) } for (let entry of Object.entries(template || {})) { let [field, value] = entry let type = typeof value if (Array.isArray(value)) { for (let v of value) { await resolveTemplate(v, serverless, cache, lookups) } } else if (type === 'object' && value != null) { if (value['Fn::Sub']) { let v = { value: await resolveFnSub(value['Fn::Sub'], serverless, cache, lookups) } await resolveTemplate(v, serverless, cache, lookups) template[field] = v.value } else if (value.Ref) { template[field] = await resolveRef(value.Ref, serverless, cache, lookups) } else if (value['Fn::ImportValue']) { let t = { value: value['Fn::ImportValue'] } await resolveTemplate(t, serverless, cache, lookups) let preValue = t.value template[field] = await resolveServices.cf(serverless.providers.aws, preValue, cache, serverless) } else { await resolveTemplate(value, serverless, cache, lookups) } } else if (type === 'string') { let [, service, key] = (value.match(/{{resolve:(secretsmanager|ssm):(.*)}}$/) || []) if (service && resolveServices[service]) { template[field] = await resolveServices[service](serverless.providers.aws, key, cache) } } } } async function resolveRef(preValue, serverless, cache, lookups) { if (serverless.service.resources.Parameters[preValue]) { return lookups[preValue] } else if (serverless.service.resources.Conditions[preValue]) { // ??? return preValue } else { return resolveServices.stack(serverless.providers.aws, preValue, cache) } } async function getLookups(serverless, cache) { let lookups = { ...(Object.entries(process.env).reduce((all, [key, value]) => { if (key.startsWith('AWS::')) { all[key] = value } return all }, {})), 'AWS::AccountId': await serverless.providers.aws.getAccountId(), 'AWS::Region': serverless.providers.aws.getRegion(), 'AWS::StackName': serverless.providers.aws.naming.getStackName() // 'Stage': serverless.service.provider.stage, // 'StageTitleCase': serverless.service.provider.stage[0].toUpperCase() + serverless.service.provider.stage.substring(1) } // Add all resources to the lookups for (let entry of Object.entries(serverless.service.resources.Resources)) { let [logicalId, value] = entry let l = await (typeExpander[value.Type] || typeExpander.default)(logicalId, value, serverless, cache, lookups) Object.assign(lookups, l || {}) } // Add all stack params to the lookups for (let one of (serverless.service.provider.stackParameters || [])) { if (one != null) { let paramDef = (serverless.service.resources.Parameters || {})[one.ParameterKey] if (paramDef && paramDef.Type.match(/AWS::SSM::Parameter/)) { let t = { value: `{{resolve:ssm:${one.ParameterValue}}}` } await resolveTemplate(t, serverless, cache, lookups) lookups[one.ParameterKey] = t.value } else { lookups[one.ParameterKey] = one.ParameterValue } } } return lookups } async function resolveFnSub(fnSub, serverless, cache, lookups) { if (lookups == null) { lookups = await getLookups(serverless, cache) } if (fnSub != null && typeof fnSub === 'object' && fnSub['Fn::Sub']) { fnSub = fnSub['Fn::Sub'] } let template = fnSub if (Array.isArray(fnSub)) { let [t, l] = fnSub template = t lookups = { ...lookups } // Resolve any new values let entries = Object.entries(l) for (let i = 0; i < entries.length; i++) { let [key, value] = entries[i] let t = { value } await resolveTemplate(t, serverless, cache, lookups) lookups[key] = t.value } } return typeof template === 'string' ? template.replace(/\${(.*?)}/g, (_, key) => { let v = lookups[key] if (typeof v !== 'string') { v = JSON.stringify(v) } return v }) : template } function getConfigEnv(serverless, file, config) { const stage = serverless.service.provider.stage const custom = serverless.service.custom[stage] ? serverless.service.custom[stage] : serverless.service.custom const leoStack = custom.leoStack || serverless.service.custom.leoStack const useSecretsManager = ((serverless.service.custom || {}).leo || {}).rsfConfigType === 'secretsmanager' let { output, lookups, permissions } = getConfigReferences(config, useSecretsManager) let hasConfig = Object.keys(output || {}).length > 0 let params = {}; // Find Stack Parameters (JSON.stringify({ leoStack: leoStack, ...lookups }).match(/\$\{(.*?)\}/g) || []).forEach((a) => { let key = a.replace(/^\$\{(.*)\}$/, '$1').split('.')[0] if (!(key in params)) { let value = serverless.pluginManager.cliOptions[key] if (value == null) { if (key.match(/^stage$/i)) { value = serverless.service.provider.stage // Proper Case if needed if (key[0] === key[0].toUpperCase()) { value = toProperCase(value) } } else if (key.match(/^AWS::.*$/i)) { // AWS var return } else if ((serverless.service.resources.Resources && serverless.service.resources.Resources[key]) || (serverless.service.resources.Parameters && serverless.service.resources.Parameters[key])) { // Stack var return } } params[key] = { Type: 'String', MinLength: 1, Description: key, Value: value } } }) // Add permissions to lambda roles to read secrets const allFunctions = serverless.service.getAllFunctions() const roles = Object.keys(allFunctions.reduce((roles, functName) => { roles[serverless.service.getFunction(functName).role] = true return roles }, {})) let secretsPermissions = [].concat(Array.from(permissions).map(p => ({ 'Fn::Sub': p }))) if (useSecretsManager) { secretsPermissions = secretsPermissions.concat({ 'Fn::Sub': 'arn:aws:secretsmanager:*:${AWS::AccountId}:secret:rsf-config-${AWS::StackName}-${AWS::Region}-*' }, leoStack ? { 'Fn::Sub': `arn:aws:secretsmanager:\${AWS::Region}:\${AWS::AccountId}:secret:rstreams-${leoStack}-*` } : undefined).filter(a => a != null) } roles.forEach(roleName => { let role = (serverless.service.resources.Resources || {})[roleName] if (role) { if (leoStack) { let alreadyHasLeoPolicy = (role.Properties.ManagedPolicyArns || []).some(p => { let policyImport = p['Fn::ImportValue'] if (policyImport && typeof policyImport === 'string' && policyImport === `${leoStack}-Policy`) { return true } else if (policyImport && typeof policyImport['Fn::Sub'] === 'string' && policyImport['Fn::Sub'] === `${leoStack}-Policy`) { return true } return false }) if (!alreadyHasLeoPolicy) { role.Properties.ManagedPolicyArns = (role.Properties.ManagedPolicyArns || []).concat({ 'Fn::ImportValue': { 'Fn::Sub': `${leoStack}-Policy` } }) } } if (secretsPermissions.length > 0) { role.Properties.Policies = (role.Properties.Policies || []).concat({ PolicyName: 'RSFSecretAccess', PolicyDocument: { Version: '2012-10-17', Statement: [ { Effect: 'Allow', Action: 'secretsmanager:GetSecretValue', Resource: secretsPermissions } ] } }) } } }) let rsfConfigName = { 'Fn::Sub': 'rsf-config-${AWS::StackName}-${AWS::Region}' } if (useSecretsManager) { // Build default replica regions let map = { RSFReplicaMap: Object.entries({ 'us-east-2': ['us-east-1'], 'us-east-1': ['us-west-2'], 'us-west-1': ['us-east-1'], 'us-west-2': ['us-east-1'], 'af-south-1': ['us-east-1'], 'ap-east-1': ['ap-south-1'], 'ap-south-1': ['ap-east-1'], 'ap-northeast-3': ['ap-east-1'], 'ap-northeast-2': ['ap-east-1'], 'ap-southeast-1': ['ap-east-1'], 'ap-southeast-2': ['ap-east-1'], 'ap-northeast-1': ['ap-east-1'], 'ca-central-1': ['us-east-1'], 'eu-central-1': ['eu-west-1'], 'eu-west-1': ['eu-central-1'], 'eu-west-2': ['eu-central-1'], 'eu-south-1': ['eu-central-1'], 'eu-west-3': ['eu-central-1'], 'eu-north-1': ['eu-central-1'], 'me-south-1': ['us-east-1'], 'sa-east-1': ['us-east-1'] }).reduce((a, [key, values]) => { if (serverless.service.custom && serverless.service.custom.leo && serverless.service.custom.leo.rsfConfigReplicationRegions && serverless.service.custom.leo.rsfConfigReplicationRegions[key]) { values = serverless.service.custom.leo.rsfConfigReplicationRegions[key] if (!Array.isArray(values)) { values = [values] } } a[key] = { values: values.map(region => ({ Region: region })) } return a }, {}) } serverless.service.resources.Mappings = Object.assign(map, serverless.service.resources.Mappings) } return { env: Object.assign(hasConfig ? { RSF_CONFIG: useSecretsManager ? rsfConfigName : { 'Fn::Sub': [ JSON.stringify(output), lookups ] } } : {}, leoStack ? (useSecretsManager ? { // Add RStreams config resource RSTREAMS_CONFIG_SECRET: { 'Fn::Sub': `rstreams-${leoStack}` } } : { RSTREAMS_CONFIG: { 'Fn::Sub': [JSON.stringify({ 'region': '${Region}', 'kinesis': '${LeoKinesisStream}', 's3': '${LeoS3}', 'firehose': '${LeoFirehoseStream}', 'resources': { 'LeoStream': '${LeoStream}', 'LeoCron': '${LeoCron}', 'LeoEvent': '${LeoEvent}', 'LeoSettings': '${LeoSettings}', 'LeoSystem': '${LeoSystem}', 'LeoS3': '${LeoS3}', 'LeoKinesisStream': '${LeoKinesisStream}', 'LeoFirehoseStream': '${LeoFirehoseStream}', 'Region': '${Region}' } }), { 'LeoStream': { 'Fn::ImportValue': { 'Fn::Sub': `${leoStack}-LeoStream` } }, 'LeoCron': { 'Fn::ImportValue': { 'Fn::Sub': `${leoStack}-LeoCron` } }, 'LeoEvent': { 'Fn::ImportValue': { 'Fn::Sub': `${leoStack}-LeoEvent` } }, 'LeoSettings': { 'Fn::ImportValue': { 'Fn::Sub': `${leoStack}-LeoSettings` } }, 'LeoSystem': { 'Fn::ImportValue': { 'Fn::Sub': `${leoStack}-LeoSystem` } }, 'LeoS3': { 'Fn::ImportValue': { 'Fn::Sub': `${leoStack}-LeoS3` } }, 'LeoKinesisStream': { 'Fn::ImportValue': { 'Fn::Sub': `${leoStack}-LeoKinesisStream` } }, 'LeoFirehoseStream': { 'Fn::ImportValue': { 'Fn::Sub': `${leoStack}-LeoFirehoseStream` } }, 'Region': { 'Fn::ImportValue': { 'Fn::Sub': `${leoStack}-Region` } } }] } }) : {}), params: params, resources: (hasConfig && useSecretsManager) ? { RSFConfig: { Type: 'AWS::SecretsManager::Secret', Properties: { Description: { 'Fn::Sub': 'Secrets for Serverless Stack ${AWS::StackName} in ${AWS::Region}' }, Name: rsfConfigName, SecretString: { 'Fn::Sub': [ JSON.stringify(output, null, 2), lookups ] }, ReplicaRegions: { 'Fn::FindInMap': ['RSFReplicaMap', { 'Ref': 'AWS::Region' }, 'values'] }, Tags: [{ Key: 'service', Value: serverless.service.service }] } } } : {} } } function populateEnvFromConfig(serverless, file, config) { let { params, env, resources } = getConfigEnv(serverless, file, config) if (env) { serverless.service.provider.environment = Object.assign(env, serverless.service.provider.environment) } if (resources) { serverless.service.resources.Resources = Object.assign(resources, serverless.service.resources.Resources) } if (params) { serverless.service.provider.stackParameters = [] .concat(serverless.service.provider.stackParameters || []) .concat(Object.entries(params) .filter(([, value]) => value.Value != null) .map(([key, value]) => { return { ParameterKey: key, ParameterValue: value.Value } })) serverless.service.resources.Parameters = Object.assign( Object.entries(params).reduce((all, [key, value]) => { let v = { ...value } delete v.Value all[key] = v return all }, {}), serverless.service.resources.Parameters) } } /** * Expand Resource Types to the needed properties for Fn::Sub */ const typeExpander = { default: () => ({}), 'AWS::Lambda::Function': async (id, fn, serverless, cache, lookups) => { let functionName = await resolveRef(id, serverless, cache, lookups) return { [id]: functionName, [id + '.Arn']: `arn:aws:lambda:${lookups['AWS::Region']}:${lookups['AWS::AccountId']}:function:${functionName}` } }, 'AWS::S3::Bucket': async (id, bucket, serverless, cache, lookups) => { let bucketName = await resolveRef(id, serverless, cache, lookups) return { [id]: bucketName, [id + '.Arn']: `arn:aws:s3:::${bucketName}` } } } module.exports = { generateConfig, getConfigFullPath, getConfigEnv, populateEnvFromConfig, resolveConfigForLocal, resolveFnSub, resolveTemplate, typeExpander }