bixlabs-generator-jhipster
Version:
Spring Boot + Angular/React/Vue in one handy generator
585 lines (536 loc) • 16.4 kB
JavaScript
/**
* Copyright 2013-2021 the original author or authors from the JHipster project.
*
* This file is part of the JHipster project, see https://www.jhipster.tech/
* for more information.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const _ = require('lodash');
const chalk = require('chalk');
const databaseTypes = require('../../jdl/jhipster/database-types');
const AURORA_DB_PASSWORD_REGEX = /^[^@"/]{8,42}$/;
const CLOUDFORMATION_STACK_NAME = /[a-zA-Z][-a-zA-Z0-9]*/;
const { MICROSERVICE, MONOLITH } = require('../../jdl/jhipster/application-types');
const SCALING_TO_CONFIG = {
low: {
fargate: {
taskCount: 1,
},
database: {
instances: 1,
},
},
medium: {
fargate: {
taskCount: 2,
},
database: {
instances: 1,
},
},
high: {
fargate: {
taskCount: 4,
},
database: {
instances: 2,
},
},
};
const PERF_TO_CONFIG = {
low: {
fargate: {
CPU: '1024',
memory: '2GB',
},
database: {
size: 'db.t2.small',
supportedEngines: [databaseTypes.MARIADB, databaseTypes.MYSQL],
},
},
medium: {
fargate: {
CPU: '2048',
memory: '4GB',
},
database: {
size: 'db.t2.medium',
supportedEngines: [databaseTypes.MARIADB, databaseTypes.MYSQL],
},
},
high: {
fargate: {
CPU: '4096',
memory: '16GB',
},
database: {
size: 'db.r4.large',
supportedEngines: [databaseTypes.MARIADB, databaseTypes.MYSQL, databaseTypes.POSTGRESQL],
},
},
};
let regionList;
/*
* All the prompting is made through Inquirer.js. Documentation here:
* https://github.com/SBoudrias/Inquirer.js/
* Use the dummy question as an example.
*/
module.exports = {
setRegionList,
askTypeOfApplication,
askRegion,
askCloudFormation,
askPerformances,
askScaling,
askVPC,
askForDBPasswords,
askForSubnets,
promptEKSClusterCreation,
askDeployNow,
};
function setRegionList(regions) {
regionList = regions;
}
function _getFriendlyNameFromTag(awsObject) {
return _.get(_(awsObject.Tags).find({ Key: 'Name' }), 'Value');
}
/**
* Ask user what type of application is to be created?
*/
function askTypeOfApplication() {
if (this.abort) return null;
const prompts = [
{
type: 'list',
name: 'applicationType',
message: 'Which *type* of application would you like to deploy?',
choices: [
{
value: MONOLITH,
name: 'Monolithic application',
},
{
value: MICROSERVICE,
name: 'Microservice application',
},
],
default: MONOLITH,
},
];
return this.prompt(prompts).then(props => {
const applicationType = props.applicationType;
this.deploymentApplicationType = props.applicationType;
if (applicationType) {
this.log(applicationType);
} else {
this.abort = true;
}
});
}
/**
* Ask user what type of Region is to be created?
*/
function askRegion() {
if (this.abort) return null;
const prompts = [
{
type: 'list',
name: 'region',
message: 'Which region?',
choices: regionList,
default: this.aws.region ? _.indexOf(regionList, this.aws.region) : this.awsFacts.defaultRegion,
},
];
return this.prompt(prompts).then(props => {
const region = props.region;
if (region) {
this.aws.region = region;
} else {
this.abort = true;
}
});
}
/**
* Ask user for CloudFormation name.
*/
function askCloudFormation() {
if (this.abort) return null;
const prompts = [
{
type: 'input',
name: 'cloudFormationName',
message: "Please enter your stack's name. (must be unique within a region)",
default: this.aws.cloudFormationName || this.baseName,
validate: input => {
if (_.isEmpty(input) || !input.match(CLOUDFORMATION_STACK_NAME)) {
return 'Stack name must contain letters, digits, or hyphens ';
}
return true;
},
},
];
return this.prompt(prompts).then(props => {
const cloudFormationName = props.cloudFormationName;
if (cloudFormationName) {
this.aws.cloudFormationName = cloudFormationName;
while (this.aws.cloudFormationName.includes('_')) {
this.aws.cloudFormationName = _.replace(this.aws.cloudFormationName, '_', '');
}
this.log(`CloudFormation Stack name will be ${this.aws.cloudFormationName}`);
} else {
this.abort = true;
}
});
}
/**
* As user to select AWS performance.
*/
function askPerformances() {
if (this.abort || this.deploymentApplicationType === 'microservice') return null;
const chainPromises = index => {
if (index === this.appConfigs.length) {
return null;
}
const config = this.appConfigs[index];
const awsConfig = this.aws.apps.find(a => a.baseName === config.baseName) || { baseName: config.baseName };
return promptPerformance.call(this, config, awsConfig).then(performance => {
awsConfig.performance = performance;
awsConfig.fargate = { ...awsConfig.fargate, ...PERF_TO_CONFIG[performance].fargate };
awsConfig.database = { ...awsConfig.database, ...PERF_TO_CONFIG[performance].database };
_.remove(this.aws.apps, a => _.isEqual(a, awsConfig));
this.aws.apps.push(awsConfig);
return chainPromises(index + 1);
});
};
return chainPromises(0);
}
function promptPerformance(config, awsConfig = { performance: 'low' }) {
if (this.abort) return null;
const prodDatabaseType = config.prodDatabaseType;
if (prodDatabaseType === databaseTypes.postgresql) {
this.log(' ⚠️ Postgresql databases are currently only supported by Aurora on high-performance database instances');
}
const performanceLevels = _(PERF_TO_CONFIG)
.keys()
.map(key => {
const perf = PERF_TO_CONFIG[key];
const isEngineSupported = perf.database.supportedEngines.includes(prodDatabaseType);
if (isEngineSupported) {
return {
name: `${_.startCase(key)} Performance \t ${chalk.green(
`Task: ${perf.fargate.CPU} CPU Units, ${perf.fargate.memory} Ram`
)}\t ${chalk.yellow(`DB: Size: ${perf.database.size}`)}`,
value: key,
short: key,
};
}
return null;
})
.compact()
.value();
const prompts = [
{
type: 'list',
name: 'performance',
message: `${chalk.red(config.baseName)} Please select your performance level`,
choices: performanceLevels,
default: awsConfig.performance,
},
];
return this.prompt(prompts).then(props => {
return props.performance;
});
}
/**
* Ask about scaling
*/
function askScaling() {
if (this.abort || this.deploymentApplicationType === MICROSERVICE) return null;
const chainPromises = index => {
if (index === this.appConfigs.length) {
return null;
}
const config = this.appConfigs[index];
const awsConfig = this.aws.apps.find(a => a.baseName === config.baseName) || { baseName: config.baseName };
return promptScaling.call(this, config, awsConfig).then(scaling => {
awsConfig.scaling = scaling;
awsConfig.fargate = { ...awsConfig.fargate, ...SCALING_TO_CONFIG[scaling].fargate };
awsConfig.database = { ...awsConfig.database, ...SCALING_TO_CONFIG[scaling].database };
_.remove(this.aws.apps, a => _.isEqual(a, awsConfig));
this.aws.apps.push(awsConfig);
return chainPromises(index + 1);
});
};
return chainPromises(0);
}
function promptScaling(config, awsConfig = { scaling: 'low' }) {
if (this.abort) return null;
const scalingLevels = _(SCALING_TO_CONFIG)
.keys()
.map(key => {
const scale = SCALING_TO_CONFIG[key];
return {
name: `${chalk.green(`${_.startCase(key)} Scaling \t\t Number of Tasks: ${scale.fargate.taskCount}`)}\t ${chalk.yellow(
`DB Instances: ${scale.database.instances}`
)}`,
value: key,
short: key,
};
})
.value();
const prompts = [
{
type: 'list',
name: 'scaling',
message: `${chalk.red(config.baseName)} Please select your scaling level`,
choices: scalingLevels,
default: awsConfig.scaling,
},
];
return this.prompt(prompts).then(props => props.scaling);
}
/**
* Ask user to select target Virtual Private Network
*/
function askVPC() {
if (this.abort) return null;
const vpcList = this.awsFacts.availableVpcs.map(vpc => {
const friendlyName = _getFriendlyNameFromTag(vpc);
return {
name: `ID: ${vpc.VpcId} (${friendlyName ? `name: '${friendlyName}', ` : ''}default: ${vpc.IsDefault}, state: ${vpc.State})`,
value: vpc.VpcId,
short: vpc.VpcId,
};
});
const prompts = [
{
type: 'list',
name: 'targetVPC',
message: 'Please select your target Virtual Private Network.',
choices: vpcList,
default: this.aws.vpc.id,
},
];
return this.prompt(prompts).then(props => {
const targetVPC = props.targetVPC;
if (targetVPC) {
this.aws.vpc.id = targetVPC;
this.aws.vpc.cidr = _.find(this.awsFacts.availableVpcs, ['VpcId', targetVPC]).CidrBlock;
} else {
this.abort = true;
}
});
}
/**
* Ask user to select availability information (availability, zones)/
*/
function askForSubnets() {
if (this.abort) return null;
const subnetList = _.map(this.awsFacts.availableSubnets, sn => {
const friendlyName = _getFriendlyNameFromTag(sn);
const formattedFriendlyName = friendlyName ? `name: '${friendlyName}', ` : '';
return {
name: `${sn.SubnetId} (${formattedFriendlyName}Availability Zone: ${sn.AvailabilityZone}, Public IP On Launch: ${
sn.MapPublicIpOnLaunch ? 'yes' : 'no'
})`,
value: sn.SubnetId,
short: sn.SubnetId,
};
});
const defaultSubnetValue = storedSubnetValue =>
storedSubnetValue || [_.get(this.awsFacts.availableSubnets, '[0].SubnetId'), _.get(this.awsFacts.availableSubnets, '[1].SubnetId')];
const validateSubnet = input =>
_.isEmpty(input) || (_.isArray(input) && input.length < 2) ? 'You must select two or more subnets' : true;
const prompts = [
{
type: 'checkbox',
name: 'elbSubnets',
message: `Which subnets should we deploy the ${chalk.yellow('Network Load Balancer (ELB)')} to?`,
choices: subnetList,
default: defaultSubnetValue(this.aws.vpc.elbSubnets),
validate: validateSubnet,
},
{
type: 'checkbox',
name: 'appSubnets',
message: `Which subnets should we deploy the ${chalk.yellow('Application & Database')} to?`,
choices: subnetList,
default: defaultSubnetValue(this.aws.vpc.appSubnets),
validate: validateSubnet,
},
];
return this.prompt(prompts).then(props => {
const publicIpOnLaunchArray = appSubnets =>
_.chain(this.awsFacts.availableSubnets)
.filter(availableSubnet => _.includes(appSubnets, availableSubnet.SubnetId))
.map('MapPublicIpOnLaunch')
.uniq()
.value();
const uniqueIPLaunch = publicIpOnLaunchArray(props.appSubnets);
const shouldAppHavePublicIP = _.head(uniqueIPLaunch);
if (uniqueIPLaunch.length !== 1) {
this.log.ok(
`⚠️ Mix of Application Subnets containing contradictory 'MapPublic Ip On Launch' values. Defaulting to '${
shouldAppHavePublicIP ? 'yes' : 'no'
}'`
);
}
this.aws.vpc.elbSubnets = props.elbSubnets;
this.aws.vpc.appSubnets = props.appSubnets;
this.aws.vpc.appSubnetsLaunchWithPublicIP = shouldAppHavePublicIP;
});
}
function askForDBPasswords() {
if (this.abort) return null;
const chainPromises = index => {
if (index === this.appConfigs.length) {
return null;
}
const config = this.appConfigs[index];
const appConfig = this.awsFacts.apps.find(a => a.baseName === config.baseName) || { baseName: config.baseName };
return promptDBPassword.call(this, appConfig).then(password => {
appConfig.database_Password = password;
_.remove(this.awsFacts.apps, a => _.isEqual(a, appConfig));
this.awsFacts.apps.push(appConfig);
return chainPromises(index + 1);
});
};
return chainPromises(0);
}
function promptDBPassword(config) {
if (config.database_Password_InSSM) return new Promise(resolve => resolve(config.database_Password));
const prompts = [
{
type: 'password',
name: 'database_Password',
message: `${chalk.red(config.baseName)} Please enter the password for the database. ${chalk.yellow(
'This value will be stored within Amazon SSM, and not within .yo-rc.json'
)}`,
validate: input =>
_.isEmpty(input) || !input.match(AURORA_DB_PASSWORD_REGEX)
? 'Password must be between 8 - 42 characters, and not contain a """, "/" or "@"'
: true,
},
];
return this.prompt(prompts).then(props => props.database_Password);
}
/**
* Create EKS stack for Micro-Services
*/
function promptEKSClusterCreation() {
if (this.abort || this.deploymentApplicationType === 'monolith') return null;
const prompts = [
{
type: 'input',
name: 'clusterName',
message: 'Name of the EKS Cluster?',
default: this.aws.clusterName || 'jhipster',
},
{
type: 'list',
name: 'kubernetesVersion',
message: 'What Kubernetes version would you like to use?',
choices: [
{
value: '1.12',
name: '1.12',
},
{
value: '1.13',
name: '1.13',
},
{
value: '1.14',
name: '1.14',
},
],
default: this.aws.kubernetesVersion || 1,
},
{
type: 'input',
name: 'nodegroupName',
message: 'Name of the Node Group?',
default: 'standard-workers',
},
{
type: 'list',
name: 'clusterRegion',
message: 'On which region do you want to deploy?',
choices: [
'ap-northeast-1',
'ap-northeast-2',
'ap-south-1',
'ap-southeast-1',
'ap-southeast-2',
'ca-central-1',
'eu-central-1',
'eu-north-1',
'eu-west-1',
'eu-west-2',
'eu-west-3',
'sa-east-1',
'us-east-1',
'us-east-2',
'us-west-1',
'us-west-2',
],
default: this.aws.clusterRegion || 15,
},
{
type: 'input',
name: 'totalNumberOfNodes',
message: 'Total number of nodes (for a static ASG)?',
default: this.aws.totalNumberOfNodes || '4',
validate: input => {
if (input.length === 0) {
return 'Instances cannot be empty';
}
const n = Math.floor(Number(input));
if (n === Infinity || String(n) !== input || n <= 0) {
return 'Please enter an integer greater than 0';
}
return true;
},
},
];
return this.prompt(prompts).then(props => {
this.clusterName = props.clusterName;
this.aws.clusterName = props.clusterName;
this.kubernetesVersion = props.kubernetesVersion;
this.aws.kubernetesVersion = props.kubernetesVersion;
this.nodegroupName = props.nodegroupName;
this.aws.nodegroupName = props.nodegroupName;
this.clusterRegion = props.clusterRegion;
this.aws.clusterRegion = props.clusterRegion;
this.totalNumberOfNodes = props.totalNumberOfNodes;
this.aws.totalNumberOfNodes = props.totalNumberOfNodes;
});
}
/**
* Ask user if they would like to deploy now?
*/
function askDeployNow() {
if (this.abort) return null;
const prompts = [
{
type: 'confirm',
name: 'deployNow',
message: 'Would you like to deploy now?',
default: true,
},
];
return this.prompt(prompts).then(props => {
this.deployNow = props.deployNow;
});
}