UNPKG

caccl-deploy

Version:

A cli tool for managing ECS/Fargate app deployments

251 lines (212 loc) 7.06 kB
const flat = require('flat'); const fetch = require('node-fetch'); const { sha1 } = require('object-hash'); const aws = require('./aws'); const { promptInfraStackName, promptCertificateArn, promptAppImage, promptKeyValuePairs, } = require('./configPrompts'); const { AppNotFound, ExistingSecretWontDelete } = require('./errors'); const { readJson } = require('./helpers'); const funcs = { fromFile(file) { const configData = readJson(file); // need to ignore this value from older deploy config files delete configData.appName; return funcs.create(configData); }, async fromUrl(url) { const resp = await fetch(url); const configData = await resp.json(); return funcs.create(configData); }, async fromSsmParams(appPrefix, keepSecretArns) { const ssmParams = await aws.getSsmParametersByPrefix(appPrefix); if (!ssmParams.length) { throw new AppNotFound( `No configuration found using app prefix ${appPrefix}`, ); } const flattened = {}; for (let i = 0; i < ssmParams.length; i += 1) { const param = ssmParams[i]; // trim off the prefix of the parameter name (path) // e.g. '/foo/bar/baz/12345' becomes 'baz/12345' const paramName = param.Name.split('/').slice(3).join('/'); const value = keepSecretArns || !param.Value.startsWith('arn:aws:secretsmanager') ? param.Value : await aws.resolveSecret(param.Value); flattened[paramName] = value; } return funcs.fromFlattened(flattened); }, fromFlattened(flattenedData) { const unflattened = flat.unflatten(flattenedData, { delimiter: '/' }); return funcs.create(unflattened); }, async generate(baseConfig = {}) { const newConfig = { ...baseConfig }; if (newConfig.infraStackName === undefined) { newConfig.infraStackName = await promptInfraStackName(); } if (newConfig.certificateArn === undefined) { newConfig.certificateArn = await promptCertificateArn(); } if (newConfig.appImage === undefined) { newConfig.appImage = await promptAppImage(); } newConfig.tags = await promptKeyValuePairs( 'tag', 'foo=bar', newConfig.tags, ); newConfig.appEnvironment = await promptKeyValuePairs( 'env var', 'FOOBAR=baz', newConfig.appEnvironment, ); console.log('\nYour new config:\n'); console.log(JSON.stringify(newConfig, null, 2)); console.log('\n'); return funcs.create(newConfig); }, async wipeExisting(ssmPrefix, ignoreMissing = true) { let existingConfig; try { existingConfig = await funcs.fromSsmParams(ssmPrefix, true); } catch (err) { if (err instanceof AppNotFound) { if (ignoreMissing) { return; } throw new AppNotFound( `No configuration found using prefix ${ssmPrefix}`, ); } } const flattened = existingConfig.flatten(); const paramsToDelete = Object.keys(flattened).map((k) => { return `${ssmPrefix}/${k}`; }); const secretsToDelete = Object.values(flattened).reduce((arns, v) => { if (v.toString().startsWith('arn:aws:secretsmanager')) { arns.push(v); } return arns; }, []); await aws.deleteSsmParameters(paramsToDelete); await aws.deleteSecrets(secretsToDelete); }, create(data) { const proxyFuncs = { flatten() { return flat(this, { delimiter: '/', safe: false, }); }, toString(pretty, flattened) { const output = flattened ? proxyFuncs.flatten.bind(this)() : this; return JSON.stringify(output, null, pretty ? '\t' : ''); }, toHash() { return sha1(this); }, tagsForAws() { if (this.tags === undefined || !Object.keys(this.tags).length) { return []; } return Object.entries(this.tags).map(([k, v]) => { return { Key: k, Value: v }; }); }, async update(appPrefix, param, value) { await proxyFuncs.syncToSsm.bind(this)(appPrefix, { [param]: value, }); this[param] = value; }, async delete(appPrefix, param) { const value = proxyFuncs.flatten.bind(this)()[param]; if (value === undefined) { throw new Error(`${param} doesn't exist`); } if (value.startsWith('arn:aws:secretsmanager')) { await aws.deleteSecrets([value]); } const paramPath = [appPrefix, param].join('/'); await aws.deleteSsmParameters([paramPath]); }, async syncToSsm(appPrefix, params) { const flattened = params === undefined ? proxyFuncs.flatten.bind(this)() : params; const paramEntries = Object.entries(flattened); const awsTags = proxyFuncs.tagsForAws.bind(this)(); for (let i = 0; i < paramEntries.length; i += 1) { const [flattenedName, rawValue] = paramEntries[i]; if (typeof rawValue === 'object') { // eslint-disable-next-line no-continue continue; } const paramName = `${appPrefix}/${flattenedName}`; let paramValue; let isSecret = false; if ( flattenedName.startsWith('appEnvironment') && !rawValue.startsWith('arn:aws:secretsmanager') ) { try { paramValue = await aws.putSecret( { Name: paramName, SecretString: rawValue, Description: 'Created and managed by caccl-deploy.', }, awsTags, ); } catch (err) { if (err instanceof ExistingSecretWontDelete) { console.log(err.message); console.log('Aborting import and cleaning up.'); await funcs.wipeExisting(appPrefix); return; } } isSecret = true; } else { paramValue = rawValue.toString(); } const paramDescription = [ 'Created and managed by caccl-deploy.', isSecret ? 'ARN value references a secretsmanager entry' : '', ].join(' '); const paramOpts = { Name: paramName, Value: paramValue, Type: 'String', Overwrite: true, Description: paramDescription, }; await aws.putSsmParameter(paramOpts, awsTags); console.log(`ssm parameter ${paramName} created`); } }, }; const handler = { has(target, property) { return property in target || property in proxyFuncs; }, get(target, property) { if (typeof property === 'string' && property in proxyFuncs) { return proxyFuncs[property].bind(target); } return target[property]; }, }; return new Proxy(data, handler); }, }; module.exports = funcs;