caccl-deploy
Version:
A cli tool for managing ECS/Fargate app deployments
236 lines (211 loc) • 6.12 kB
JavaScript
const chalk = require('chalk');
const figlet = require('figlet');
const prompts = require('prompts');
const aws = require('./aws');
const { conf } = require('./conf');
const { UserCancel, NoPromptChoices } = require('./errors');
const { looksLikeSemver, validSSMParamName } = require('./helpers');
const prompt = async (question, continueOnCancel) => {
return prompts(question, {
onCancel: () => {
if (!continueOnCancel) {
throw new UserCancel();
}
},
});
};
const promptFuncs = {
confirm: async (message, defaultsToYes) => {
const response = await prompt({
type: 'confirm',
name: 'yesorno',
initial: defaultsToYes,
message,
});
return response.yesorno;
},
confirmProductionOp: async (yes) => {
if (yes) {
return true;
}
const prodAccounts = conf.get('productionAccounts');
if (prodAccounts === undefined || !prodAccounts.length) {
return true;
}
const accountId = await aws.getAccountId();
if (!prodAccounts.includes(accountId)) {
return true;
}
console.log(chalk.redBright(figlet.textSync('Production Account!')));
try {
const ok = await promptFuncs.confirm(
'\nPlease confirm you wish to proceed\n',
);
return ok;
} catch (err) {
if (err instanceof UserCancel) {
return false;
}
throw err;
}
},
promptAppName: async () => {
const appName = await prompt({
type: 'text',
name: 'value',
message: 'Enter a name for your app',
validate: (v) => {
return !validSSMParamName(v)
? 'app name can only contain alphanumeric and/or the characters ".-_"'
: true;
},
});
return appName.value;
},
promptInfraStackName: async () => {
const infraStacks = await aws.getInfraStackList();
if (infraStacks.length === 1) {
return infraStacks[0];
}
const infraStackChoices = infraStacks.map((value) => {
return {
title: value,
value,
};
});
if (!infraStackChoices.length) {
throw new NoPromptChoices('No infrastructure stacks');
}
const infraStackName = await prompt({
type: 'select',
name: 'value',
message: 'Select a base infrastructure stack to deploy to',
choices: infraStackChoices,
});
return infraStackName.value;
},
promptCertificateArn: async () => {
const certList = await aws.getAcmCertList();
const certChoices = certList.map((cert) => {
return {
title: cert.DomainName,
value: cert.CertificateArn,
};
});
if (!certChoices.length) {
throw new NoPromptChoices('No ACM certificates to choose from');
}
const certificateArn = await prompt({
type: 'select',
name: 'value',
message: 'Select the hostname associated with your ACM certificate',
choices: certChoices,
});
return certificateArn.value;
},
promptAppImage: async () => {
const inputType = await prompt({
type: 'select',
name: 'value',
message: 'How would you like to select your image?',
choices: [
{
title: 'Select from a list of ECR repos',
value: 'select',
},
{
title: 'Enter image id string',
value: 'string',
},
],
});
let appImage;
switch (inputType.value) {
case 'string': {
const inputString = await prompt({
type: 'text',
name: 'value',
message: 'Enter the image id',
});
appImage = inputString.value;
break;
}
case 'select': {
const repoList = await aws.getRepoList();
const repoChoices = repoList.map((value) => {
return {
title: value,
value,
};
});
if (!repoChoices.length) {
throw new NoPromptChoices('No ECR repositories');
}
const repoChoice = await prompt({
type: 'select',
name: 'value',
message: 'Select the ECR repo',
choices: repoChoices,
});
const images = await aws.getRepoImageList(repoChoice.value);
const imageTagsChoices = images.reduce((choices, image) => {
const releaseTag = image.imageTags.find((tag) => {
return looksLikeSemver(tag);
});
const appImageValue = aws.createEcrArn({
region: aws.getCurrentRegion(),
account: image.registryId,
repoName: repoChoice.value,
imageTag: releaseTag,
});
if (releaseTag) {
choices.push({
title: releaseTag,
value: appImageValue,
});
}
return choices;
}, []);
if (!imageTagsChoices.length) {
throw new NoPromptChoices('No valid image tags to choose from');
}
const imageTagChoice = await prompt({
type: 'select',
name: 'value',
message: 'Select a release tag',
choices: imageTagsChoices,
});
appImage = imageTagChoice.value;
break;
}
default:
break;
}
return appImage;
},
promptKeyValuePairs: async (label, example, current = {}) => {
const pairs = { ...current };
const displayList = [];
Object.entries(pairs).forEach(([k, v]) => {
displayList.push(`${k}=${v}`);
});
console.log(`Current ${label}(s):\n${displayList.join('\n')}`);
const newEntry = await prompt({
type: 'text',
name: 'value',
message: `Enter a new ${label}, e.g. ${example}. Leave empty to continue.`,
validate: (v) => {
return v !== '' && v.split('=').length !== 2
? 'invalid entry format'
: true;
},
});
if (newEntry.value !== '') {
const [newKey, newValue] = newEntry.value.split('=');
pairs[newKey] = newValue;
return promptFuncs.promptKeyValuePairs(label, example, pairs);
}
return pairs;
},
};
module.exports = promptFuncs;