serverless-leo
Version:
Serverless plugin for leo microservices
293 lines (259 loc) • 8.85 kB
JavaScript
const fs = require('fs')
const path = require('path')
const { fetchAll } = require('./utils')
const regions = [
'us-east-2',
'us-east-1',
'us-west-1',
'us-west-2',
'af-south-1',
'ap-east-1',
'ap-south-1',
'ap-northeast-3',
'ap-northeast-2',
'ap-southeast-1',
'ap-southeast-2',
'ap-northeast-1',
'ca-central-1',
'eu-central-1',
'eu-west-1',
'eu-west-2',
'eu-south-1',
'eu-west-3',
'eu-north-1',
'me-south-1',
'sa-east-1'
]
const regionRegex = new RegExp(`(${regions.join('|')})`, 'g')
function tokenize(service, value, extra, stageRegex) {
let tokens = {
stage: [],
region: []
}
let { Description: desc, Tags: tags } = extra || {}
let rsfTokenTag = (tags || []).find(t => t.Key === 'rsf:token')
let tokenizedValue
if (rsfTokenTag) {
try {
rsfTokenTag.Value = Buffer.from(rsfTokenTag.Value, 'base64').toString()
} catch (e) {
// not base64
}
tokenizedValue = rsfTokenTag.Value.replace(/__(.*?)__/g, (all, capture) => {
return `\${${capture}}`
})
} else {
let tokenMatches = desc && desc.match(/rsf-token:(.*?):rsf-token/)
if (tokenMatches) {
tokenizedValue = tokenMatches[1]
} else {
tokenizedValue = value.replace(stageRegex, (str, args) => {
const stage = str[0] === str[0].toUpperCase() ? 'Stage' : 'stage'
tokens.stage.push(str)
return `\${${stage}}`
}).replace(regionRegex, (str) => {
tokens.region.push(str)
return '${region}'
})
}
}
if (!tokenizedValue.startsWith(service + '::')) {
tokenizedValue = `${service}::${tokenizedValue}`
}
let [_service, tokenizedKey, type, opts] = tokenizedValue.split(/(?<!AWS)::/)
let options = (opts || '').split(';').reduce((all, one) => {
let [key, value] = one.split('=')
if (key !== '') {
all[key] = value == null ? true : value
}
return all
}, {})
let name = options.name
delete options.name
let optsStr = Object.entries(options).reduce((all, [key, value]) => {
return all.concat(`${key}=${value}`)
}, []).join(';')
tokenizedValue = [service, tokenizedKey, type, optsStr].filter(a => a).join('::')
if (name == null) {
name = tokenizedKey
}
name = name.replace(new RegExp(`^${service}::`), '')
.replace(/(\${.*?})/g, '')
.replace(/[-_ //\\\\.]+/g, '_')
.replace(/(^_|_$)/g, '')
name = name[0].toLowerCase() + name.slice(1)
return {
service: service,
value: value,
tokenizedValue: tokenizedValue,
tokens: tokens,
tags: tags || [],
desc: desc || '',
name: name
}
}
function mergeTokens(all, token) {
let key = token.tokenizedValue
if (!(key in all)) {
all[key] = {
service: token.service,
tokenizedValue: token.tokenizedValue,
tokens: {},
values: [],
name: token.name,
tags: [],
desc: []
}
}
let entry = all[key]
const tokens = entry.tokens
entry.values = entry.values.concat(token.value)
if (token.desc) {
entry.desc = entry.desc.concat(token.desc)
}
if (token.tags) {
entry.tags = entry.tags.concat(token.tags)
}
Object.entries(token.tokens || {}).forEach(([key, value]) => {
const existing = tokens[key] || []
tokens[key] = existing.concat(value)
})
return all
}
let aws = require('aws-sdk')
let prompt = require('prompt-sync')({ sigint: true })
async function editConfig(serverless, configPath, region) {
let configDef = {}
if (fs.existsSync(configPath)) {
configDef = require(configPath)
}
let allTokenized
let awsResourcesCachePath = path.resolve(serverless.serviceDir, `.rsf/resource-cache.json`)
if (fs.existsSync(awsResourcesCachePath)) {
let stat = fs.statSync(awsResourcesCachePath)
let duration = Math.floor((Date.now() - stat.mtimeMs) / 1000)
let validCacheDuration = (+process.env.RSF_CACHE_SECONDS) || 1800
if (duration < validCacheDuration) {
try {
allTokenized = require(awsResourcesCachePath)
} catch (e) {
// Error getting cache
}
}
}
if (allTokenized == null) {
if (region == null) {
region = prompt('Region: [us-east-1] ') || 'us-east-1'
}
let ssmClient = new aws.SSM({
region: region
})
let secretsClient = new aws.SecretsManager({
region: region
})
let cfClient = new aws.CloudFormation({
region: region
})
let [ssm, secrets, cfexports, rsfGlobalConfig] = await Promise.all([
fetchAll(t => ssmClient.describeParameters({ NextToken: t, MaxResults: 50 }).promise()),
fetchAll(t => secretsClient.listSecrets({ NextToken: t, MaxResults: 100 }).promise()),
fetchAll(t => cfClient.listExports({ NextToken: t }).promise()),
ssmClient.getParameter({ Name: '/rsf/global/config' }).promise().catch(() => { return { Parameter: { Type: 'String', Value: '{}' } } })
])
if (rsfGlobalConfig.Parameter.Type == 'StringList') {
rsfGlobalConfig = {
stages: rsfGlobalConfig.Parameter.Value.split(',')
}
} else if (rsfGlobalConfig.Parameter.Type == 'String') {
rsfGlobalConfig = JSON.parse(rsfGlobalConfig.Parameter.Value)
} else {
rsfGlobalConfig = {}
}
rsfGlobalConfig = {
stages: ['dev', 'test', 'stage', 'staging', 'production', 'prod'],
...rsfGlobalConfig
}
let rsfStages = Array.from(new Set([].concat(rsfGlobalConfig.stages || []).concat((serverless.service.custom && serverless.service.custom.leo && serverless.service.custom.leo.rsfConfigStages) || [])))
const stageRegex = new RegExp(`(${rsfStages.join('|')})`, 'ig')
let ssmTokenized = ssm.Parameters.map(v => {
return tokenize('ssm', v.Name, v, stageRegex)
}).reduce(mergeTokens, {})
let secretsTokenized = secrets.SecretList.map(v => {
return tokenize('secret', v.Name, v, stageRegex)
}).reduce(mergeTokens, {})
let cfTokenized = cfexports.Exports.map(v => {
return tokenize('cf', v.Name, v, stageRegex)
}).reduce(mergeTokens, {})
allTokenized = {
...ssmTokenized,
...secretsTokenized,
...cfTokenized
}
fs.mkdirSync(path.dirname(awsResourcesCachePath), { recursive: true })
fs.writeFileSync(awsResourcesCachePath, JSON.stringify(allTokenized, null, 2))
}
let stackResources = serverless.service.resources || {}
Object.entries(stackResources.Resources || {}).map(([key, entry]) => {
let type = ''
return tokenize('stack', key + (type ? `::${type}` : ''), entry, /^$/)
}).reduce(mergeTokens, allTokenized)
const ParameterTypes = {
'String': 'string',
'CommaDelimitedList': 'string[]',
'Number': 'number',
'List<Number>': 'number[]'
}
Object.entries(stackResources.Parameters || {}).map(([key, entry]) => {
let type = ParameterTypes[entry.Type]
return tokenize('stack', key + (type ? `::${type}` : ''), entry, /^$/)
}).reduce(mergeTokens, allTokenized)
function search(value) {
if (!value) {
value = prompt('Enter a search value: ')
}
const values = value.split(/ +/).map(v => new RegExp(v, 'i'))
return Object.values(allTokenized).filter(data => {
return values.every(value => data.name.match(value) || data.desc.some(d => d.match(value) || data.tags.some(t => t.Value.match(value))))
}).sort((a, b) => a.name.localeCompare(b.name))
}
function printSearch(matches) {
if (matches.length) {
matches.forEach((m, i) => {
console.log(`${i + 1}) ${m.name} (${m.service.toUpperCase()})`)
})
} else {
console.log('No matches!')
}
}
let cmd
let lastSearch
const exitCommand = 'done'
do {
cmd = prompt(`search${lastSearch ? '|add' : ''}|done: `)
if (cmd === exitCommand || cmd === '') {
// Nothing
} else if (cmd === 'show') {
console.log(JSON.stringify(configDef, null, 2))
} else if (lastSearch && cmd.match(/\d+/)) {
let entry = lastSearch[parseInt(cmd) - 1]
if (entry == null) {
console.log('Invalid Selection')
} else {
// TODO: what if the key already exists?
console.log(`Adding ${entry.name} (${entry.service.toUpperCase()})`)
configDef[entry.name] = entry.tokenizedValue
}
} else {
lastSearch = search(cmd)
printSearch(lastSearch)
}
} while (cmd !== exitCommand && cmd != '')
console.log(configDef)
// Save file to the config location
if (Object.keys(configDef).length > 0) {
fs.writeFileSync(configPath, JSON.stringify(configDef, null, 2))
}
}
module.exports = {
editConfig
}