bixlabs-generator-jhipster
Version:
Spring Boot + Angular/React/Vue in one handy generator
707 lines (666 loc) • 25.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 fs = require('fs');
const shelljs = require('shelljs');
const BaseGenerator = require('../generator-base');
const docker = require('../docker-base');
const dockerCli = require('../docker-cli');
const dockerUtils = require('../docker-utils');
const dockerPrompts = require('../docker-prompts');
const constants = require('../generator-constants');
const databaseTypes = require('../../jdl/jhipster/database-types');
const fileUtils = require('../../jdl/utils/file-utils');
const { MONOLITH } = require('../../jdl/jhipster/application-types');
const { MAVEN, GRADLE } = require('../../jdl/jhipster/build-tool-types');
const cacheTypes = require('../../jdl/jhipster/cache-types');
const NO_CACHE_PROVIDER = cacheTypes.NO;
const prompts = require('./prompts');
const awsClient = require('./aws-client');
const AWS_SSM_ARTIFACT = 'aws-java-sdk-ssm';
const AWS_SSM_GROUP = 'com.amazonaws';
const SPRING_CLOUD_GROUP = 'org.springframework.cloud';
const SPRING_CLOUD_ARTIFACT = 'spring-cloud-context';
const BASE_TEMPLATE_PATH = 'base.template.yml';
const APP_TEMPLATE_PATH = baseName => `${baseName}.template.yml`;
const AWSSSM_CONFIG_PATH = (directory, packageFolder) =>
`${directory}/${constants.SERVER_MAIN_SRC_DIR}/${packageFolder}/bootstrap/AwsSSMConfiguration.java`;
const SPRING_FACTORIES_PATH = directory => `${directory}/${constants.SERVER_MAIN_RES_DIR}/META-INF/spring.factories`;
const BOOTSTRAP_PATH = directory => `${directory}/${constants.SERVER_MAIN_RES_DIR}/config/bootstrap-aws.yml`;
const BASE_TEMPLATE_FILENAME = `${BASE_TEMPLATE_PATH}.ejs`;
const APP_TEMPLATE_FILENAME = 'application.template.yml.ejs';
const AWSSSM_CONFIG_FILENAME = 'AwsSSMConfiguration.java.ejs';
const SPRING_FACTORIES_FILENAME = 'spring.factories.ejs';
const BOOTSTRAP_FILENAME = 'bootstrap-aws.yml.ejs';
/**
* Returns the password property to be stored within Amazon SSM
* @param stacKName Name of the root stack
* @param applicationName Name of the application property
* @returns {string}
* Full property path
*/
const ssmPasswordProperty = (stacKName, applicationName) => `/${stacKName}/${applicationName}/spring.datasource.password`;
module.exports = class extends BaseGenerator {
constructor(args, opts) {
super(args, opts);
// This adds support for a `--skip-build` flag
this.option('skip-build', {
desc: 'Disables the project build step',
type: Boolean,
defaults: false,
});
// This adds support for a `--skip-upload` flag
this.option('skip-upload', {
desc: 'Skips the Docker Image Tag + Upload process',
type: Boolean,
defaults: false,
});
}
get initializing() {
return {
validateFromCli() {
this.checkInvocationFromCLI();
},
bonjour() {
this.log(chalk.bold('This AWS generator will help you deploy your JHipster app as a Docker container on AWS.'));
},
option() {
this.deployNow = this.options.skipInstall;
this.skipUpload = this.options.skipUpload;
this.skipBuild = this.options.skipBuild;
},
getConfig() {
if (fileUtils.doesFileExist('awsConstants.json')) {
this.aws = JSON.parse(fs.readFileSync('awsConstants.json', { encoding: 'utf-8' }));
} else {
this.aws = {
apps: [],
vpc: {},
dockerLogin: {
accountId: null,
password: null,
},
};
}
this.defaultAppsFolders = this.aws.apps.map(a => a.baseName);
},
checkDocker: docker.checkDocker,
checkAwsCredentials() {
if (this.abort) return;
const done = this.async();
let profile = process.env.AWS_PROFILE;
if (!profile) {
profile = 'default';
}
awsClient
.saveCredentialsInAWS(profile)
.then(() => {
this.log.ok(`AWS credentials using profile ${chalk.bold(profile)}.`);
done();
})
.catch(() => {
this.log.error(chalk.red(`No AWS credentials found for profile ${chalk.bold(profile)}`));
done('Please setup AWS credentials; https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html');
});
},
initAwsStuff() {
awsClient.initAwsStuff();
},
setOutputs() {
dockerCli.setOutputs(
data => this.log(chalk.white(data.toString().trim())),
data => this.log.error(data.toString().trim())
);
awsClient.CF().setOutputs(message => this.log(message));
},
fetchRegion() {
if (this.abort) return;
const done = this.async();
this.awsFacts = {
apps: [],
defaultRegion: awsClient.DEFAULT_REGION,
database_Password_InSSM: false,
};
awsClient
.listRegions()
.then(regions => {
const regionsLabel = _.map(regions, r => r.RegionName);
prompts.setRegionList(regionsLabel);
done();
})
.catch(err => {
this.log.error(err);
this.abort = true;
done();
});
},
};
}
get prompting() {
return {
askTypeOfApplication: prompts.askTypeOfApplication,
askDirectoryPath: dockerPrompts.askForPath,
askForApps: dockerPrompts.askForApps,
getAppConfig: dockerPrompts.loadConfigs,
promptEKSClusterCreation: prompts.promptEKSClusterCreation,
createEKSCluster() {
if (this.deploymentApplicationType === MONOLITH) return;
const done = this.async();
this.log.ok('Initialising Elastic Kubernetes Service (EKS)️. This can take up to 15 minutes depending on load....');
shelljs.exec(
`eksctl create cluster --name=${this.clusterName} --nodes=${this.totalNumberOfNodes}` +
` --version=${this.kubernetesVersion} --region=${this.clusterRegion} --nodegroup-name=${this.nodegroupName}`,
{ silent: false },
(code, stdout, stderr) => {
if (stderr) {
this.log(stderr);
}
done();
}
);
},
createECR() {
if (this.deploymentApplicationType === MONOLITH) return;
const done = this.async();
this.log.ok('Initialising Elastic Repository Service (ERS) ...');
this.appConfigs.forEach(app => {
shelljs.exec(
`aws ecr create-repository --repository-name=${app.baseName} --region=${this.clusterRegion}`,
{ silent: false },
(code, stdout, stderr) => {
if (stderr) {
this.log(stderr);
}
done();
}
);
});
},
finishMicroServiceFlow() {
if (this.deploymentApplicationType === MONOLITH) return;
const done = this.async();
this.log.ok(chalk.green('EKS and ECRs created. Please use the Kubernetes Sub-generator (jhipster kubernetes) to deploy.`'));
this.abort = true;
done();
},
askRegion: prompts.askRegion,
initAwsAndLoadVPCs() {
awsClient.initAwsStuff(this.aws.region);
return awsClient.listVpcs().then(listOfVpcs => {
this.awsFacts.availableVpcs = listOfVpcs;
});
},
askVPC: prompts.askVPC,
initSubnets() {
const done = this.async;
return awsClient
.listSubnets(this.aws.vpc.id)
.then(subnets => {
this.awsFacts.availableSubnets = subnets;
done();
})
.catch(() => {
this.log.error('Unable to fetch the subnets');
done();
});
},
askForSubnets: prompts.askForSubnets,
askCloudFormation: prompts.askCloudFormation,
askPerformances: prompts.askPerformances,
askScaling: prompts.askScaling,
retrievePassword() {
const done = this.async;
// Attempts to retrieve a previously set database password from SSM.
const promises = this.aws.apps.map(app =>
awsClient
.SSM()
.getSSMParameter(ssmPasswordProperty(this.aws.cloudFormationName, app.baseName))
.then(password => {
if (password) {
let fact = this.awsFacts.apps.find(a => a.baseName === app.baseName);
if (_.isUndefined(fact)) {
fact = {
baseName: app.baseName,
};
this.awsFacts.apps.push(fact);
}
fact.database_Password = password;
fact.database_Password_InSSM = true;
}
})
.catch(() => {
this.log.error('Unable to fetch the SSM Parameters');
})
);
return Promise.all(promises)
.then(() => done())
.catch(() => {
this.abort = true;
done();
});
},
askForDBPassword: prompts.askForDBPasswords,
askDeployNow: prompts.askDeployNow,
};
}
get configuring() {
return {
bonjour() {
if (this.abort) return;
this.log(chalk.bold('🔧🛠️ AWS configuring'));
},
purgeAwsApps() {
if (this.abort) return;
this.aws.apps = this.aws.apps.filter(app => this.appConfigs.find(conf => conf.baseName === app.baseName));
},
setBucketName() {
if (this.abort) return;
this.aws.s3BucketName =
this.aws.s3BucketName || awsClient.sanitizeBucketName(`${this.aws.cloudFormationName}_${new Date().getTime()}`);
},
getDockerLogin() {
if (this.abort) return null;
const done = this.async;
return awsClient
.getDockerLogin()
.then(token => {
this.log.ok('ECR Auth token has been retrieved.');
this.aws.dockerLogin = token;
done();
})
.catch(error => {
this.log.error(error);
this.abort = true;
done();
});
},
};
}
get default() {
return {
bonjour() {
if (this.abort) return;
this.log(chalk.bold('AWS default'));
},
updateBaseName() {
if (this.abort) return;
this.appConfigs.forEach(config => {
config.awsBaseName = config.baseName.toLowerCase().replace(/[^a-z^\d]/, '');
});
},
showAwsCacheWarning() {
if (this.abort) return;
this.appConfigs.forEach(config => {
if (config.cacheProvider !== NO_CACHE_PROVIDER) {
this.log(
chalk.yellow(
`Warning ${config.baseName} is using a cache provider, scaling will not be available. Refer to an AWS native scaling service.`
)
);
}
});
},
addAWSSpringDependencies() {
if (this.abort) return;
this.appConfigs.forEach(config => {
const directory = `${this.directoryPath}${config.appFolder}`;
if (config.buildTool === MAVEN) {
this.addMavenDependencyInDirectory(directory, AWS_SSM_GROUP, AWS_SSM_ARTIFACT);
this.addMavenDependencyInDirectory(directory, SPRING_CLOUD_GROUP, SPRING_CLOUD_ARTIFACT);
} else if (config.buildTool === GRADLE) {
this.addGradleDependencyInDirectory(directory, 'compile', AWS_SSM_GROUP, AWS_SSM_ARTIFACT);
this.addGradleDependencyInDirectory(directory, 'compile', SPRING_CLOUD_GROUP, SPRING_CLOUD_ARTIFACT);
}
});
},
setAuroraParameters() {
if (this.abort) return;
this.appConfigs.forEach(appConfig => {
const app = this.aws.apps.find(a => a.baseName === appConfig.baseName);
const postgresqlType = databaseTypes.POSTGRESQL;
app.dbType = appConfig.prodDatabaseType;
app.auroraEngine = appConfig.prodDatabaseType === postgresqlType ? 'aurora-postgresql' : 'aurora-mysql';
app.auroraFamily = appConfig.prodDatabaseType === postgresqlType ? 'aurora-postgresql9.6' : 'aurora-mysql5.7';
app.auroraClusterParameterGroupName =
appConfig.prodDatabaseType === postgresqlType ? 'default.aurora-postgresql9.6' : 'default.aurora-mysql5.7';
app.auroraDbParam = appConfig.prodDatabaseType === postgresqlType ? 'check_function_bodies: 0' : 'sql_mode: IGNORE_SPACE';
});
},
springProjectChanges() {
if (this.abort) return;
this.appConfigs.forEach(config => {
const directory = `${this.directoryPath}${config.appFolder}`;
this.temp = {
baseName: config.baseName,
packageName: config.packageName,
};
this.template(AWSSSM_CONFIG_FILENAME, AWSSSM_CONFIG_PATH(directory, config.packageFolder));
this.template(SPRING_FACTORIES_FILENAME, SPRING_FACTORIES_PATH(directory));
this.template(BOOTSTRAP_FILENAME, BOOTSTRAP_PATH(directory));
});
},
generateCloudFormationTemplate() {
if (this.abort) return;
this.template(BASE_TEMPLATE_FILENAME, BASE_TEMPLATE_PATH);
this.aws.apps.forEach(config =>
this.template(APP_TEMPLATE_FILENAME, APP_TEMPLATE_PATH(config.baseName), null, {}, { aws: this.aws, app: config })
);
},
};
}
_writeFileErrorHandler(generator) {
fs.writeFile('awsConstants.json', JSON.stringify(this.aws), function (error) {
if (error) {
generator.log.error(`There was an error writing the awsConstants.json file: ${error.message}`);
} else {
generator.log(chalk.green.bold('Configuration saved at awsConstants.json'));
}
});
}
_uploadTemplateToAWS(filename, path) {
if (this.abort) return null;
const done = this.async;
return awsClient
.uploadTemplate(this.aws.s3BucketName, filename, path)
.then(result => {
this.log.ok(`${chalk.bold(filename)} has been updated to the S3 Bucket and can be found here: ${chalk.underline(result.Location)}`);
done();
return result;
})
.catch(error => {
this.log.error(error.message);
this.abort = true;
done();
});
}
get end() {
return {
checkAndBuildImages() {
if (this.abort || !this.deployNow || this.skipBuild) return null;
const done = this.async();
const cwd = process.cwd();
const promises = this.appConfigs.map(config =>
dockerUtils.checkAndBuildImages.call(this, {
cwd: `${this.directoryPath}${config.appFolder}/`,
appConfig: { buildTool: config.buildTool },
})
);
return Promise.all(promises)
.then(() => {
process.chdir(cwd);
done();
})
.catch(() => {
this.abort = true;
done();
});
},
createS3Bucket() {
if (this.abort || !this.deployNow) return null;
const done = this.async;
return awsClient
.createS3Bucket(this.aws.s3BucketName, this.aws.region)
.then(result => {
this.aws.s3bucketLocation = result.Location;
this.log.ok(`The S3 Bucket ${chalk.bold(this.aws.s3BucketName)} has been created.`);
done();
})
.catch(error => {
this.log.error(`Could not create the S3 bucket : ${error.message}`);
this.abort = true;
done();
});
},
uploadBaseTemplate() {
if (this.abort || !this.deployNow) return null;
const done = this.async;
return this._uploadTemplateToAWS('base.template.yml', BASE_TEMPLATE_PATH).then(result => {
this.aws.s3BaseTemplate = result;
done();
});
},
uploadAppTemplate() {
if (this.abort || !this.deployNow) return null;
const done = this.async;
const promises = this.aws.apps.map(config =>
this._uploadTemplateToAWS(APP_TEMPLATE_PATH(config.baseName), APP_TEMPLATE_PATH(config.baseName))
);
return Promise.all(promises)
.then(() => done())
.catch(() => {
this.abort = true;
done();
});
},
createOrUpdateStack() {
if (this.abort || !this.deployNow) return null;
const done = this.async;
return awsClient
.CF()
.getStack(this.aws.cloudFormationName)
.then(() => {
const promises = this.aws.apps.map(app =>
awsClient
.CF()
.getStack(app.stackId)
.then(() => {
this.log.ok(`Existing CloudFormation Stack for app ${chalk.bold(app.baseName)} Found ☁️`);
})
.catch(() => {
this.log.error(`Issue retrieving nested stack for app ${chalk.bold(app.baseName)} doesn't exist`);
})
);
return Promise.all(promises)
.then(() => done())
.catch(() => {
this.abort = true;
done();
});
})
.catch(() => {
this.log.ok('Initialising CloudFormation Stack ☁️ (this can take up to 15 minutes depending on load)');
const databasePasswords = this.awsFacts.apps.map(a =>
awsClient.CF().cfParameter(`${a.baseName}DBPassword`, a.database_Password)
);
return awsClient
.CF()
.createCloudFormationStack(this.aws.cloudFormationName, this.aws.s3BaseTemplate.Location, databasePasswords)
.then(result => {
this.log.ok(`The CloudFormation Stack ${chalk.bold(result.StackId)} has been created.`);
this.aws.apps.forEach(app => {
const stack = result.nestedStacks.find(stack => stack.appName.includes(app.baseName));
app.stackId = stack.stackId;
});
done();
})
.catch(error => {
this.log.error(`There was an error creating the stack: ${error.message}`);
this.abort = true;
done();
});
});
},
getElasticContainerRepositoryName() {
if (this.abort || !this.deployNow) return null;
const done = this.async;
const promises = this.aws.apps.map(app =>
awsClient
.CF()
.getEcrId(app.stackId)
.then(result => {
app.EcrRepositoryName = result;
this.log.ok(`ECR Repository ID for app ${chalk.bold(app.baseName)} was found: ${result}`);
})
.catch(error => {
this.log.error(`Couldn't get ECR Repository Id for app ${chalk.bold(app.baseName)}: ${error.message}`);
this.abort = true;
})
);
return Promise.all(promises).then(() => done());
},
setSSMDatabasePassword() {
if (this.abort || !this.deployNow) return null;
const done = this.async;
const promises = this.aws.apps.map(app => {
const fact = this.awsFacts.apps.find(fact => fact.baseName === app.baseName);
if (fact.database_Password_InSSM) {
return null;
}
const passwordProperty = ssmPasswordProperty(this.aws.cloudFormationName, app.baseName);
return awsClient
.SSM()
.setSSMParameter(passwordProperty, fact.database_Password, `Database master password for ${app.baseName}`)
.then(() => {
this.log.ok(`Password has been set in ASM Parameter: ${passwordProperty}`);
})
.catch(err => {
this.log.error(`Issue setting SSM property. Error: ${err.message}`);
});
});
return Promise.all(promises)
.then(() => done())
.catch(() => {
this.abort = true;
done();
});
},
getEcrRepositoryURI() {
if (this.abort || !this.deployNow) return null;
const done = this.async;
const promises = this.aws.apps.map(app =>
awsClient
.ECR()
.getEcrRepositoryURI(app.EcrRepositoryName)
.then(uri => {
app.EcrRepositoryUri = uri;
this.log.ok(`ECR Repository URI for ${app.baseName} was found: ${uri}`);
})
.catch(error => {
this.log.error(`Couldn't get ECR URI for ${app.baseName} : ${error.message}`);
this.abort = true;
})
);
return Promise.all(promises)
.then(() => done())
.catch(() => {
this.abort = true;
done();
});
},
tagDockerImage() {
if (this.abort || !this.deployNow || this.skipUpload) return null;
const done = this.async;
const promises = this.aws.apps.map(app => {
const from = `${app.baseName.toLowerCase()}:latest`;
const to = `${app.EcrRepositoryUri}:latest`;
return dockerCli
.tagImage(from, to)
.then(() => {
app.dockerImageTag = to;
this.log.ok(`The Docker image was tagged: ${chalk.bold(to)}`);
})
.catch(error => {
this.log.error(error);
this.log.error(`There was an error tagging the Docker image: ${error.message}`);
});
});
return Promise.all(promises)
.then(() => done())
.catch(() => {
this.abort = true;
done();
});
},
loginToAws() {
if (this.abort || !this.deployNow || this.skipUpload) return null;
const done = this.async;
return dockerCli
.loginToAws(this.aws.region, this.aws.dockerLogin.accountId, this.aws.dockerLogin.username, this.aws.dockerLogin.password)
.then(() => {
this.log.ok(`Docker is now connected to your account in the region ${this.aws.region}.`);
done();
})
.catch(() => {
this.log.error("Couldn't connect to AWS with Docker");
this.abort = true;
done();
});
},
pushDockerImage() {
if (this.abort || !this.deployNow || this.skipUpload) return null;
const done = this.async;
const promises = this.aws.apps.map(app => {
const repository = `${app.EcrRepositoryUri}:latest`;
return dockerCli
.pushImage(repository)
.then(() => {
this.log.ok(`Image is now pushed to repository ${repository}.`);
})
.catch(() => {
this.log.error("Couldn't push image to AWS ECR Repository");
});
});
return Promise.all(promises)
.then(() => done())
.catch(() => {
this.abort = true;
done();
});
},
updateStack() {
if (this.abort || !this.deployNow) return null;
const done = this.async;
const databasePasswords = this.awsFacts.apps.map(a => awsClient.CF().cfParameter(`${a.baseName}DBPassword`, a.database_Password));
const nestedStackIds = this.aws.apps.map(app => app.stackId);
this.log.ok('Updating Existing CloudFormation Stack ☁️');
return awsClient
.CF()
.updateCloudFormationStack(
this.aws.cloudFormationName,
nestedStackIds,
this.aws.s3BaseTemplate.Location,
databasePasswords,
'true'
)
.then(result => {
this.log(`The CloudFormation Stack ${chalk.bold(this.aws.cloudFormationName)} has been updated`);
if (_.has(result, 'Stacks[0].Outputs')) {
this.log.ok('Applications Accessible at Load Balancers:');
_(result)
.get('Stacks[0].Outputs')
.filter(output => output.OutputKey.startsWith('LoadBalancerOutput'))
.forEach(output => this.log(`\thttp://${output.OutputValue}`));
}
done();
})
.catch(error => {
this.log.error(`There was an error updating the stack: ${error.message}\n`);
this.log.error(error);
this.abort = true;
done();
});
},
saveConf() {
delete this.aws.dockerLogin;
this._writeFileErrorHandler(this);
},
};
}
};