first commit
This commit is contained in:
+35
@@ -0,0 +1,35 @@
|
||||
const { build } = require('gluegun');
|
||||
|
||||
/**
|
||||
* Create the cli and kick it off
|
||||
*/
|
||||
async function run(argv) {
|
||||
// create a CLI runtime
|
||||
const cli = build()
|
||||
.brand('pwcli')
|
||||
.src(__dirname)
|
||||
.plugins('./node_modules', { matching: 'pwcli-*', hidden: true })
|
||||
.plugin(__dirname + '/../node_modules/@lenne.tech/gluegun-menu/dist', {
|
||||
commandFilePattern: '*.js',
|
||||
extensionFilePattern: '*.js'
|
||||
})
|
||||
.help({
|
||||
name: 'help',
|
||||
alias: 'helpmeplease',
|
||||
hidden: true,
|
||||
dashed: true,
|
||||
run: toolbox => toolbox.print.info('No help for you!')
|
||||
}) // provides default for help, h, --help, -h
|
||||
.version() // provides default for version, v, --version, -v
|
||||
.create();
|
||||
// enable the following method if you'd like to skip loading one of these core extensions
|
||||
// this can improve performance if they're not necessary for your project:
|
||||
// .exclude(['meta', 'strings', 'print', 'filesystem', 'semver', 'system', 'prompt', 'http', 'template', 'patching', 'package-manager'])
|
||||
// and run it
|
||||
const toolbox = await cli.run(argv);
|
||||
|
||||
// send it back (for testing, mostly)
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
module.exports = { run }
|
||||
@@ -0,0 +1,11 @@
|
||||
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu';
|
||||
|
||||
module.exports = {
|
||||
name: 'deployment',
|
||||
alias: ['d'],
|
||||
description: 'Description for deployment (d)',
|
||||
hidden: true,
|
||||
run: async (toolbox: GluegunMenuToolbox) => {
|
||||
await toolbox.menu.showMenu('deployment');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,251 @@
|
||||
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu'
|
||||
const chalk = require('chalk');
|
||||
const { AutoComplete, Input, Confirm } = require('enquirer');
|
||||
import { Octokit } from "@octokit/core";
|
||||
import { GluegunPrint } from 'gluegun';
|
||||
const os = require("os");
|
||||
|
||||
const STACK_DEFAULTS = {
|
||||
STACK_ENVIRONMENT_NAME_TAG: 'test',
|
||||
CPU_SIZE: 512,
|
||||
MEMORY_SIZE: '1GB',
|
||||
HOSTED_ZONE_WITHOUT_TLD: 'photowall-test',
|
||||
DOMAIN_NAME_WITHOUT_TLD: '.photowall-test',
|
||||
PRIMARY_CERTIFICATE_ARN:
|
||||
'arn:aws:acm:eu-west-1:954747537408:certificate/12e33b74-da14-430d-a7ed-f62fa7113112',
|
||||
STATIC_CDN_CERTIFICATE_ARN:
|
||||
'arn:aws:acm:us-east-1:954747537408:certificate/223550cf-e404-4ff1-85bb-703b936c9347',
|
||||
DB_SECURITY_GROUP_ID: 'sg-001b3667edf19e1ce',
|
||||
GITHUB_REPO: 'photowall',
|
||||
GITHUB_USER: 'Photowall',
|
||||
STATIC_CDN_HOSTNAME: `''`,
|
||||
MAX_CONTAINERS: 1,
|
||||
MIN_CONTAINERS: 1,
|
||||
AWS_KEY_ENVIRONMENT: 'staging'
|
||||
}
|
||||
|
||||
type Stack = {
|
||||
name: string
|
||||
status: string
|
||||
drift_status: string
|
||||
branch: string
|
||||
updated: string
|
||||
}
|
||||
|
||||
const getDownloadFileFromRepoCommand = (token: string, org: string, repo: string, path: string) => {
|
||||
const file = path.split('/').pop();
|
||||
return `curl -o /tmp/${file} -H 'Authorization: token ${token} ' -H 'Accept: application/vnd.github.v3.raw' -O -L https://api.github.com/repos/${org}/${repo}/contents/${path}`;
|
||||
}
|
||||
|
||||
const getCreateStackCommand = (stackName: string, branch: string, templateFile: string, dockerImage: string, githubToken: string, composerAuth: string) => {
|
||||
const stack = 'photowall';
|
||||
const stackDomain = stackName.replace('photowall-', '');
|
||||
const cmd = [
|
||||
`aws cloudformation create-stack --stack-name ${stackName}`,
|
||||
`--template-body ${templateFile}`,
|
||||
`--capabilities CAPABILITY_NAMED_IAM`,
|
||||
`--tags Key=EnvironmentName,Value=test`,
|
||||
`--parameters`,
|
||||
`ParameterKey=EnvironmentName,ParameterValue=${stackDomain}`,
|
||||
`ParameterKey=Image,ParameterValue=${dockerImage}`,
|
||||
`ParameterKey=HostedZoneNameWithOutTLD,ParameterValue=${STACK_DEFAULTS.HOSTED_ZONE_WITHOUT_TLD}`,
|
||||
`ParameterKey=DomainNameWithoutTld,ParameterValue=${stackDomain}${STACK_DEFAULTS.DOMAIN_NAME_WITHOUT_TLD}`,
|
||||
`ParameterKey=PrimaryCertificateArn,ParameterValue='${STACK_DEFAULTS.PRIMARY_CERTIFICATE_ARN}'`,
|
||||
`ParameterKey=GitHubRepo,ParameterValue=${STACK_DEFAULTS.GITHUB_REPO}`,
|
||||
`ParameterKey=GitHubUser,ParameterValue=${STACK_DEFAULTS.GITHUB_USER}`,
|
||||
`ParameterKey=GitHubBranch,ParameterValue=${branch}`,
|
||||
`ParameterKey=GitHubToken,ParameterValue=${githubToken}`,
|
||||
`ParameterKey=ComposerAuth,ParameterValue="${composerAuth}"`,
|
||||
`ParameterKey=CpuSize,ParameterValue=${STACK_DEFAULTS.CPU_SIZE}`,
|
||||
`ParameterKey=MemorySize,ParameterValue=${STACK_DEFAULTS.MEMORY_SIZE}`,
|
||||
`ParameterKey=MinContainers,ParameterValue=${STACK_DEFAULTS.MIN_CONTAINERS}`,
|
||||
`ParameterKey=MaxContainers,ParameterValue=${STACK_DEFAULTS.MAX_CONTAINERS}`,
|
||||
`ParameterKey=StaticCdnHostName,ParameterValue=${STACK_DEFAULTS.STATIC_CDN_HOSTNAME}`,
|
||||
`ParameterKey=StaticCdnCertificateArn,ParameterValue=${STACK_DEFAULTS.STATIC_CDN_CERTIFICATE_ARN}`,
|
||||
`ParameterKey=AwsAccessKeyParamNameEnvironment,ParameterValue=${STACK_DEFAULTS.AWS_KEY_ENVIRONMENT}`,
|
||||
`ParameterKey=DatabaseSecurityGroup,ParameterValue=${STACK_DEFAULTS.DB_SECURITY_GROUP_ID}`,
|
||||
];
|
||||
|
||||
return cmd.join(' ');
|
||||
};
|
||||
|
||||
const getBranchName = async (print: GluegunPrint, token: string) => {
|
||||
const input = new Input({
|
||||
type: 'input',
|
||||
name: 'branch',
|
||||
message: 'What github branch should be deployed?'
|
||||
});
|
||||
const responseBranch = await input.run();
|
||||
|
||||
// First ask what branch should be deployed
|
||||
const octokit = new Octokit({ auth: token });
|
||||
|
||||
try {
|
||||
const response = await octokit.request("GET /repos/{owner}/{repo}/branches/{branch}", {
|
||||
owner: "photowall",
|
||||
repo: "photowall",
|
||||
branch: responseBranch
|
||||
});
|
||||
if (response.status === 200) {
|
||||
return responseBranch;
|
||||
}
|
||||
} catch (e) {
|
||||
print.error('Not a valid branch on photowall repo');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getConfirmation = async (branchName: string, stackName: string) => {
|
||||
const url = stackName.replace('photowall-', '');
|
||||
const confirm = new Confirm({
|
||||
name: 'confirm',
|
||||
message: `Will create ${chalk.yellow(url + '.photowall-test.xx')} and publish ${chalk.green(branchName)} to it, continue?`
|
||||
});
|
||||
|
||||
try {
|
||||
return confirm.run();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subsection1 menu
|
||||
*/
|
||||
module.exports = {
|
||||
name: 'create',
|
||||
alias: ['c'],
|
||||
description: 'Create new testserver on aws (c)',
|
||||
hidden: true,
|
||||
run: async (toolbox: GluegunMenuToolbox) => {
|
||||
const { system, strings, print, filesystem } = toolbox;
|
||||
|
||||
// Load settings
|
||||
const config = await filesystem.readAsync(`${os.homedir()}/.pwcli_settings`, "json");
|
||||
if (!config) {
|
||||
print.error(`
|
||||
|
||||
No config found, please run [ ${chalk.cyan('pwcli s')} ] or setup from the menu.
|
||||
|
||||
`);
|
||||
if (toolbox.fromMenu()) {
|
||||
await toolbox.menu.showMenu()
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let branchName = null;
|
||||
while (!branchName) {
|
||||
branchName = await getBranchName(print, config.gh_token);
|
||||
}
|
||||
|
||||
const stacks = JSON.parse(
|
||||
strings.trim(await system.run(`aws cloudformation list-stacks`))
|
||||
)
|
||||
const teststacks: Stack[] = await Promise.all(
|
||||
stacks.StackSummaries.map(async item => {
|
||||
if (
|
||||
item.StackName.indexOf('photowall-test-') > -1 &&
|
||||
[
|
||||
'CREATE_COMPLETE',
|
||||
'UPDATE_COMPLETE',
|
||||
'UPDATE_IN_PROGRESS',
|
||||
'UPDATE_FAILED',
|
||||
'CREATE_FAILED',
|
||||
'CREATE_IN_PROGRESS'
|
||||
].includes(item.StackStatus)
|
||||
) {
|
||||
const pipeline = JSON.parse(
|
||||
strings.trim(
|
||||
await system.run(
|
||||
`aws codepipeline get-pipeline --name ${item.StackName}Pipeline`
|
||||
)
|
||||
)
|
||||
)
|
||||
const branch = pipeline.pipeline.stages.filter(stage => {
|
||||
return stage.name === 'Source'
|
||||
})[0].actions[0].configuration.Branch
|
||||
const lastUpdated = pipeline.metadata.updated
|
||||
return {
|
||||
name: item.StackName,
|
||||
status: item.StackStatus,
|
||||
drift_status: item.DriftInformation.StackDriftStatus,
|
||||
branch: branch,
|
||||
updated: lastUpdated
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null)
|
||||
})
|
||||
)
|
||||
|
||||
const curratedStacks: Stack[] = teststacks.filter(Boolean)
|
||||
const options = curratedStacks.map(item => {
|
||||
return item.name;
|
||||
});
|
||||
|
||||
const input = new Input({
|
||||
type: 'input',
|
||||
name: 'stack',
|
||||
message: `Insert name of the new stack (${chalk.yellow('photowall-test-xx-x')})`
|
||||
});
|
||||
const stackName = await input.run();
|
||||
|
||||
if (options.includes(stackName)) {
|
||||
print.error(`
|
||||
|
||||
The stack ${chalk.yellow(stackName)} is already taken, try the update command instead.
|
||||
|
||||
`);
|
||||
if (toolbox.fromMenu()) {
|
||||
await toolbox.menu.showMenu('deployment testservers');
|
||||
}
|
||||
} else if (stackName.indexOf('photowall-test-') !== 0) {
|
||||
print.error(`
|
||||
|
||||
The stack must begin with ${chalk.yellow('photowall-test-')}.
|
||||
|
||||
`);
|
||||
if (toolbox.fromMenu()) {
|
||||
await toolbox.menu.showMenu('deployment testservers');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
|
||||
const shouldRun = await getConfirmation(branchName, stackName);
|
||||
|
||||
if (shouldRun) {
|
||||
// Download files to /tmp
|
||||
await system.run(getDownloadFileFromRepoCommand(config.gh_token, 'Photowall', 'photowall', 'cloudformation/ecr-lifecycle-policy.json'));
|
||||
await system.run(getDownloadFileFromRepoCommand(config.gh_token, 'Photowall', 'photowall', 'cloudformation/ecs-service.yaml'));
|
||||
|
||||
const aws_account_id = strings.trim(await system.run(`aws sts get-caller-identity --query Account --output text`));
|
||||
const aws_region = strings.trim(await system.run(`aws configure get region`));
|
||||
const github_token = strings.trim(await system.run(`aws ssm get-parameter --with-decryption --name /github/CODE_PIPELINE_TOKEN --output text --query Parameter.Value`));
|
||||
|
||||
print.fancy(await system.run(`aws ecr create-repository --repository ${stackName}`));
|
||||
print.fancy(await system.run(`aws ecr get-login-password --region ${aws_region} | docker login --username AWS --password-stdin ${aws_account_id}.dkr.ecr.${aws_region}.amazonaws.com`));
|
||||
print.fancy(await system.run(`docker pull nginx`));
|
||||
print.fancy(await system.run(`docker tag nginx ${aws_account_id}.dkr.ecr.${aws_region}.amazonaws.com/${stackName}:latest`));
|
||||
print.fancy(await system.run(`docker push ${aws_account_id}.dkr.ecr.${aws_region}.amazonaws.com/${stackName}:latest`));
|
||||
|
||||
const dockerImage = `${aws_account_id}.dkr.ecr.${aws_region}.amazonaws.com/${stackName}`;
|
||||
let composerAuthToken = await system.run(`aws ssm get-parameter --with-decryption --name /github/COMPOSER_TOKEN --output text --query Parameter.Value`);
|
||||
composerAuthToken = composerAuthToken.replace('\n', '');
|
||||
const composer_auth = `'{\"github-oauth\":{\"github.com\":\"${composerAuthToken}\"}}'`;
|
||||
|
||||
const createCommand = getCreateStackCommand(stackName, branchName, 'file://ecs-service.yaml', dockerImage, github_token, composer_auth);
|
||||
print.fancy(await system.run(createCommand, {cwd: '/tmp/', trim: true}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`e`, e)
|
||||
}
|
||||
|
||||
if (toolbox.fromMenu()) {
|
||||
await toolbox.menu.showMenu('deployment testservers')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu'
|
||||
import { GluegunSystem, print } from 'gluegun';
|
||||
const chalk = require('chalk');
|
||||
const { AutoComplete, Input, Confirm } = require('enquirer');
|
||||
|
||||
type Stack = {
|
||||
name: string,
|
||||
status: string,
|
||||
drift_status: string,
|
||||
branch: string,
|
||||
updated: string,
|
||||
};
|
||||
|
||||
const getConfirmation = async (stackName: string) => {
|
||||
const confirm = new Confirm({
|
||||
name: 'confirm',
|
||||
message: `The stack ${chalk.yellow(stackName)} will be ${chalk.red.bold('deleted')}`
|
||||
});
|
||||
|
||||
try {
|
||||
return confirm.run();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const confirmCommand = async (cmd: string) => {
|
||||
const confirm = new Confirm({
|
||||
name: 'confirm',
|
||||
message: `RUN COMMAND > [${chalk.yellow(cmd)}] ?`,
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await confirm.run();
|
||||
if (resp) {
|
||||
return cmd;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const runCommand = async (system: GluegunSystem, cmd : string | null) => {
|
||||
if (cmd) {
|
||||
return system.run(cmd);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Subsection1 menu
|
||||
*/
|
||||
module.exports = {
|
||||
name: 'delete',
|
||||
alias: ['d'],
|
||||
description: 'Choose a testserver to delete (d)',
|
||||
hidden: true,
|
||||
run: async (toolbox: GluegunMenuToolbox) => {
|
||||
|
||||
const { system, strings, print } = toolbox;
|
||||
|
||||
const stacks = JSON.parse(
|
||||
strings.trim(await system.run(`aws cloudformation list-stacks`))
|
||||
)
|
||||
const teststacks: Stack[] = await Promise.all(
|
||||
stacks.StackSummaries.map(async item => {
|
||||
if (
|
||||
item.StackName.indexOf('photowall-test-') > -1 &&
|
||||
[
|
||||
'CREATE_COMPLETE',
|
||||
'UPDATE_COMPLETE',
|
||||
'UPDATE_IN_PROGRESS',
|
||||
'UPDATE_FAILED',
|
||||
'CREATE_FAILED',
|
||||
'CREATE_IN_PROGRESS'
|
||||
].includes(item.StackStatus)
|
||||
) {
|
||||
const pipeline = JSON.parse(
|
||||
strings.trim(
|
||||
await system.run(
|
||||
`aws codepipeline get-pipeline --name ${item.StackName}Pipeline`
|
||||
)
|
||||
)
|
||||
)
|
||||
const branch = pipeline.pipeline.stages.filter(stage => {
|
||||
return stage.name === 'Source'
|
||||
})[0].actions[0].configuration.Branch
|
||||
const lastUpdated = pipeline.metadata.updated
|
||||
return {
|
||||
name: item.StackName,
|
||||
status: item.StackStatus,
|
||||
drift_status: item.DriftInformation.StackDriftStatus,
|
||||
branch: branch,
|
||||
updated: lastUpdated
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null)
|
||||
})
|
||||
)
|
||||
|
||||
const curratedStacks: Stack[] = teststacks.filter(Boolean)
|
||||
const options = curratedStacks.map(item => {
|
||||
return item.name
|
||||
})
|
||||
|
||||
const prompt = new AutoComplete({
|
||||
name: 'testserver',
|
||||
message: 'Choose a server',
|
||||
limit: options.length - 1,
|
||||
initial: options.length - 1,
|
||||
choices: options
|
||||
});
|
||||
|
||||
const stackName = await prompt.run();
|
||||
|
||||
const shouldRun = await getConfirmation(stackName);
|
||||
|
||||
if (shouldRun) {
|
||||
await runCommand(system, await confirmCommand(`aws cloudformation delete-stack --stack-name ${stackName}`));
|
||||
await runCommand(system, await confirmCommand(`aws ecr delete-repository --force --repository-name ${stackName}`));
|
||||
await runCommand(system, await confirmCommand(`aws logs delete-log-group --log-group-name /aws/codebuild/${stackName}`));
|
||||
await runCommand(system, await confirmCommand(`aws logs delete-log-group --log-group-name ${stackName}`));
|
||||
const artifacts = strings.trim(
|
||||
await system.run(`aws s3 ls | grep ${stackName}-artifactbucket | awk '{print $3}'`)
|
||||
).split('\n');
|
||||
for (const artifact of artifacts) {
|
||||
await runCommand(system, await confirmCommand(`aws s3 rb --force s3://${artifact}`));
|
||||
}
|
||||
}
|
||||
|
||||
if (toolbox.fromMenu()) {
|
||||
await toolbox.menu.showMenu('deployment testservers');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu'
|
||||
const tto = require('terminal-table-output').create();
|
||||
|
||||
type Stack = {
|
||||
name: string,
|
||||
status: string,
|
||||
drift_status: string,
|
||||
branch: string,
|
||||
updated: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Subsection1 menu
|
||||
*/
|
||||
module.exports = {
|
||||
name: 'status',
|
||||
alias: ['s'],
|
||||
description: 'Show status for available testservers (s)',
|
||||
hidden: true,
|
||||
run: async (toolbox: GluegunMenuToolbox) => {
|
||||
|
||||
const { system, strings } = toolbox
|
||||
|
||||
const stacks = JSON.parse(strings.trim(await system.run(`aws cloudformation list-stacks`)));
|
||||
const teststacks: Stack[] = await Promise.all(stacks.StackSummaries.map(async (item) => {
|
||||
if (item.StackName.indexOf('photowall-test-') > -1 && ['CREATE_COMPLETE', 'UPDATE_COMPLETE', 'UPDATE_IN_PROGRESS', 'UPDATE_FAILED', 'CREATE_FAILED', 'CREATE_IN_PROGRESS'].includes(item.StackStatus)) {
|
||||
const pipeline = JSON.parse(strings.trim(await system.run(`aws codepipeline get-pipeline --name ${item.StackName}Pipeline`)));
|
||||
const branch = pipeline.pipeline.stages.filter(stage => {
|
||||
return stage.name === 'Source';
|
||||
})[0].actions[0].configuration.Branch;
|
||||
const lastUpdated = pipeline.metadata.updated;
|
||||
return {
|
||||
name: item.StackName,
|
||||
status: item.StackStatus,
|
||||
drift_status: item.DriftInformation.StackDriftStatus,
|
||||
branch: branch,
|
||||
updated: lastUpdated,
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}));
|
||||
|
||||
const curratedStacks: Stack[] = teststacks.filter(Boolean);
|
||||
|
||||
const status = (status: string) => {
|
||||
switch(status) {
|
||||
case 'UPDATE_FAILED':
|
||||
case 'CREATE_FAILED':
|
||||
return ` (💔) `;
|
||||
case 'UPDATE_IN_PROGRESS':
|
||||
case 'CREATE_IN_PROGRESS':
|
||||
return ` (🟡) `;
|
||||
case 'UPDATE_COMPLETE':
|
||||
case 'CREATE_COMPLETE':
|
||||
return ` (🟢) `;
|
||||
}
|
||||
}
|
||||
|
||||
tto.line('-');
|
||||
tto.pushrow(['| status', 'stack', 'branch', 'updated'])
|
||||
.line('-');
|
||||
for (const stack of curratedStacks) {
|
||||
tto.pushrow([status(stack.status), stack.name, stack.branch, stack.updated]);
|
||||
}
|
||||
tto.print(true);
|
||||
|
||||
if (toolbox.fromMenu()) {
|
||||
await toolbox.menu.showMenu('deployment testservers');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu';
|
||||
|
||||
/**
|
||||
* Section1 menu
|
||||
*/
|
||||
module.exports = {
|
||||
name: 'testservers',
|
||||
alias: ['ts'],
|
||||
description: 'Deploy to testservers (ts)',
|
||||
hidden: true,
|
||||
run: async (toolbox: GluegunMenuToolbox) => {
|
||||
await toolbox.menu.showMenu('deployment testservers');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu'
|
||||
const chalk = require('chalk');
|
||||
const { AutoComplete, Input, Confirm } = require('enquirer');
|
||||
import { Octokit } from "@octokit/core";
|
||||
import { GluegunPrint } from 'gluegun';
|
||||
const os = require("os");
|
||||
|
||||
const STACK_DEFAULTS = {
|
||||
STACK_ENVIRONMENT_NAME_TAG: 'test',
|
||||
CPU_SIZE: 512,
|
||||
MEMORY_SIZE: '1GB',
|
||||
HOSTED_ZONE_WITHOUT_TLD: 'photowall-test',
|
||||
PRIMARY_CERTIFICATE_ARN:
|
||||
'arn:aws:acm:eu-west-1:954747537408:certificate/12e33b74-da14-430d-a7ed-f62fa7113112',
|
||||
STATIC_CDN_CERTIFICATE_ARN:
|
||||
'arn:aws:acm:us-east-1:954747537408:certificate/223550cf-e404-4ff1-85bb-703b936c9347',
|
||||
DB_SECURITY_GROUP_ID: 'sg-001b3667edf19e1ce',
|
||||
STATIC_CDN_HOSTNAME: `''`,
|
||||
MAX_CONTAINERS: 1,
|
||||
MIN_CONTAINERS: 1,
|
||||
AWS_KEY_ENVIRONMENT: 'staging'
|
||||
}
|
||||
|
||||
type Stack = {
|
||||
name: string
|
||||
status: string
|
||||
drift_status: string
|
||||
branch: string
|
||||
updated: string
|
||||
}
|
||||
|
||||
const getDownloadFileFromRepoCommand = (token: string, org: string, repo: string, path: string) => {
|
||||
const file = path.split('/').pop();
|
||||
return `curl -o /tmp/${file} -H 'Authorization: token ${token} ' -H 'Accept: application/vnd.github.v3.raw' -O -L https://api.github.com/repos/${org}/${repo}/contents/${path}`;
|
||||
}
|
||||
|
||||
const getLifeCyclePolicyCommand = (stackName: string, policyFile: string) => {
|
||||
return `aws ecr put-lifecycle-policy --repository-name ${stackName} --lifecycle-policy-text "${policyFile}"`;
|
||||
}
|
||||
|
||||
const getUpdateStackCommand = (stackName: string, branch: string, templateFile: string) => {
|
||||
const cmd = [
|
||||
`aws cloudformation update-stack --stack-name ${stackName}`,
|
||||
`--template-body ${templateFile}`,
|
||||
`--capabilities CAPABILITY_NAMED_IAM`,
|
||||
`--tags Key=EnvironmentName,Value=test`,
|
||||
`--parameters`,
|
||||
`ParameterKey=EnvironmentName,UsePreviousValue=true`,
|
||||
`ParameterKey=Image,UsePreviousValue=true`,
|
||||
`ParameterKey=HostedZoneNameWithOutTLD,UsePreviousValue=true`,
|
||||
`ParameterKey=DomainNameWithoutTld,UsePreviousValue=true`,
|
||||
`ParameterKey=PrimaryCertificateArn,UsePreviousValue=true`,
|
||||
`ParameterKey=GitHubRepo,UsePreviousValue=true`,
|
||||
`ParameterKey=GitHubUser,UsePreviousValue=true`,
|
||||
`ParameterKey=GitHubBranch,ParameterValue=${branch}`,
|
||||
`ParameterKey=GitHubToken,UsePreviousValue=true`,
|
||||
`ParameterKey=ComposerAuth,UsePreviousValue=true`,
|
||||
`ParameterKey=CpuSize,ParameterValue=${STACK_DEFAULTS.CPU_SIZE}`,
|
||||
`ParameterKey=MemorySize,ParameterValue=${STACK_DEFAULTS.MEMORY_SIZE}`,
|
||||
`ParameterKey=MinContainers,ParameterValue=${STACK_DEFAULTS.MIN_CONTAINERS}`,
|
||||
`ParameterKey=MaxContainers,ParameterValue=${STACK_DEFAULTS.MAX_CONTAINERS}`,
|
||||
`ParameterKey=StaticCdnHostName,ParameterValue=${STACK_DEFAULTS.STATIC_CDN_HOSTNAME}`,
|
||||
`ParameterKey=StaticCdnCertificateArn,ParameterValue=${STACK_DEFAULTS.STATIC_CDN_CERTIFICATE_ARN}`,
|
||||
`ParameterKey=AwsAccessKeyParamNameEnvironment,ParameterValue=${STACK_DEFAULTS.AWS_KEY_ENVIRONMENT}`,
|
||||
`ParameterKey=DatabaseSecurityGroup,ParameterValue=${STACK_DEFAULTS.DB_SECURITY_GROUP_ID}`
|
||||
];
|
||||
return cmd.join(' ');
|
||||
}
|
||||
|
||||
const getBranchName = async (print: GluegunPrint, token: string) => {
|
||||
const input = new Input({
|
||||
type: 'input',
|
||||
name: 'branch',
|
||||
message: 'What github branch should be deployed?'
|
||||
});
|
||||
const responseBranch = await input.run();
|
||||
|
||||
// First ask what branch should be deployed
|
||||
const octokit = new Octokit({ auth: token });
|
||||
|
||||
try {
|
||||
const response = await octokit.request("GET /repos/{owner}/{repo}/branches/{branch}", {
|
||||
owner: "photowall",
|
||||
repo: "photowall",
|
||||
branch: responseBranch
|
||||
});
|
||||
if (response.status === 200) {
|
||||
return responseBranch;
|
||||
}
|
||||
} catch (e) {
|
||||
print.error('Not a valid branch on photowall repo');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getConfirmation = async (branchName: string, stackName: string) => {
|
||||
const confirm = new Confirm({
|
||||
name: 'confirm',
|
||||
message: `The branch ${chalk.green(branchName)} will deploy to ${chalk.yellow(stackName)}`
|
||||
});
|
||||
|
||||
try {
|
||||
return confirm.run();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subsection1 menu
|
||||
*/
|
||||
module.exports = {
|
||||
name: 'update',
|
||||
alias: ['u'],
|
||||
description: 'Update stack with different branch (u)',
|
||||
hidden: true,
|
||||
run: async (toolbox: GluegunMenuToolbox) => {
|
||||
const { system, strings, print, filesystem } = toolbox;
|
||||
|
||||
// Load settings
|
||||
const config = await filesystem.readAsync(`${os.homedir()}/.pwcli_settings`, "json");
|
||||
if (!config) {
|
||||
print.error(`
|
||||
|
||||
No config found, please run [ ${chalk.cyan('pwcli s')} ] or setup from the menu.
|
||||
|
||||
`);
|
||||
if (toolbox.fromMenu()) {
|
||||
await toolbox.menu.showMenu()
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let branchName = null;
|
||||
while (!branchName) {
|
||||
branchName = await getBranchName(print, config.gh_token);
|
||||
}
|
||||
|
||||
const stacks = JSON.parse(
|
||||
strings.trim(await system.run(`aws cloudformation list-stacks`))
|
||||
)
|
||||
const teststacks: Stack[] = await Promise.all(
|
||||
stacks.StackSummaries.map(async item => {
|
||||
if (
|
||||
item.StackName.indexOf('photowall-test-') > -1 &&
|
||||
[
|
||||
'CREATE_COMPLETE',
|
||||
'UPDATE_COMPLETE',
|
||||
'UPDATE_IN_PROGRESS',
|
||||
'UPDATE_FAILED',
|
||||
'CREATE_FAILED',
|
||||
'CREATE_IN_PROGRESS'
|
||||
].includes(item.StackStatus)
|
||||
) {
|
||||
const pipeline = JSON.parse(
|
||||
strings.trim(
|
||||
await system.run(
|
||||
`aws codepipeline get-pipeline --name ${item.StackName}Pipeline`
|
||||
)
|
||||
)
|
||||
)
|
||||
const branch = pipeline.pipeline.stages.filter(stage => {
|
||||
return stage.name === 'Source'
|
||||
})[0].actions[0].configuration.Branch
|
||||
const lastUpdated = pipeline.metadata.updated
|
||||
return {
|
||||
name: item.StackName,
|
||||
status: item.StackStatus,
|
||||
drift_status: item.DriftInformation.StackDriftStatus,
|
||||
branch: branch,
|
||||
updated: lastUpdated
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null)
|
||||
})
|
||||
)
|
||||
|
||||
const curratedStacks: Stack[] = teststacks.filter(Boolean)
|
||||
const options = curratedStacks.map(item => {
|
||||
return item.name;
|
||||
});
|
||||
|
||||
const prompt = new AutoComplete({
|
||||
name: 'testserver',
|
||||
message: 'Choose a server',
|
||||
limit: options.length - 1,
|
||||
initial: options.length - 1,
|
||||
choices: options
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
// Download files to /tmp
|
||||
await system.run(getDownloadFileFromRepoCommand(config.gh_token, 'Photowall', 'photowall', 'cloudformation/ecr-lifecycle-policy.json'));
|
||||
await system.run(getDownloadFileFromRepoCommand(config.gh_token, 'Photowall', 'photowall', 'cloudformation/ecs-service.yaml'));
|
||||
|
||||
const stackName = await prompt.run();
|
||||
|
||||
const lifeCycleCommand = getLifeCyclePolicyCommand(stackName, `file://ecr-lifecycle-policy.json`);
|
||||
const updateStackCommand = getUpdateStackCommand(stackName, branchName, `file://ecs-service.yaml`);
|
||||
|
||||
const shouldRun = await getConfirmation(branchName, stackName);
|
||||
|
||||
if (shouldRun) {
|
||||
let output = await system.run(lifeCycleCommand, {cwd: '/tmp/', trim: true});
|
||||
print.fancy(output);
|
||||
output = await system.run(updateStackCommand, {cwd: '/tmp/', trim: true});
|
||||
print.fancy(output);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log(`e`, e)
|
||||
}
|
||||
|
||||
if (toolbox.fromMenu()) {
|
||||
await toolbox.menu.showMenu('deployment testservers')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu';
|
||||
import { GluegunCommand } from 'gluegun'
|
||||
|
||||
|
||||
const command: GluegunCommand = {
|
||||
name: 'pwcli',
|
||||
run: async (toolbox: GluegunMenuToolbox) => {
|
||||
const { print } = toolbox;
|
||||
print.info('Welcome to photowall CLI for everything, almost 😬');
|
||||
await toolbox.menu.showMenu();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = command;
|
||||
@@ -0,0 +1,131 @@
|
||||
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu';
|
||||
import chalk = require('chalk');
|
||||
import { system } from 'gluegun';
|
||||
const { Confirm, Input } = require('enquirer');
|
||||
|
||||
const checkCLIProgram = async (toolbox: GluegunMenuToolbox, cmd: string) => {
|
||||
const {print, system, strings} = toolbox;
|
||||
try {
|
||||
const result = strings.trim(await system.run(`command -v ${cmd}`));
|
||||
return true;
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: 'settings',
|
||||
alias: ['se'],
|
||||
description: 'Setup settings for pwcli (se)',
|
||||
hidden: true,
|
||||
run: async (toolbox: GluegunMenuToolbox) => {
|
||||
|
||||
const {print, system, strings} = toolbox;
|
||||
|
||||
print.info(chalk.yellow(`
|
||||
|
||||
This will help you setup everything needed to run pwcli on a mac (maby linux).
|
||||
All settings will be saved to ${chalk.cyan('~/.pwcli_settings')}
|
||||
|
||||
First step is to install aws cli [https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html]
|
||||
and configuring it. Install it either with brew or do as specified in the link.
|
||||
|
||||
`));
|
||||
|
||||
// Check if brew is installed
|
||||
// if (await checkCLIProgram(toolbox, 'brew')) {
|
||||
// print.info(`\nDetected ${chalk.green('brew')}`);
|
||||
// } else {
|
||||
// print.info(`\nRequired program ${chalk.green('brew')} is not installed.`);
|
||||
|
||||
// if (await (new Confirm({
|
||||
// name: 'confirm',
|
||||
// message: `Do you want pwcli to install it?`
|
||||
// })).run()) {
|
||||
// print.info(`/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`);
|
||||
// // system.run('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"');
|
||||
// } else {
|
||||
// if (await (new Confirm({
|
||||
// name: 'confirm',
|
||||
// message: `Do you want to install it your self?`
|
||||
// })).run()) {
|
||||
// system.run('open https://brew.sh');
|
||||
// } else {
|
||||
// print.info('😔 ok, your choice..');
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (await (new Confirm({
|
||||
// name: 'confirm',
|
||||
// message: `Is ${chalk.green('brew')} installed?`
|
||||
// })).run()) {
|
||||
// print.info('Great, lets continue!');
|
||||
// } else {
|
||||
// print.info('😔 ok, your choice..');
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (await checkCLIProgram(toolbox, 'asdf')) {
|
||||
// print.info(`\nDetected ${chalk.green('aws cli')}`);
|
||||
// } else {
|
||||
// print.info(`\nRequired program ${chalk.green('aws cli')} is not installed.`);
|
||||
// if (await (new Confirm({
|
||||
// name: 'confirm',
|
||||
// message: `Do you want pwcli to install it?`
|
||||
// })).run()) {
|
||||
// print.info(`brew install awscli`);
|
||||
// // system.run('brew install awscli');
|
||||
// } else {
|
||||
// if (await (new Confirm({
|
||||
// name: 'confirm',
|
||||
// message: `Do you want to install it your self?`
|
||||
// })).run()) {
|
||||
// system.run('open https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html');
|
||||
// } else {
|
||||
// print.info('😔 ok, your choice..');
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (await (new Confirm({
|
||||
// name: 'confirm',
|
||||
// message: `Is ${chalk.green('aws cli')} installed and configured?`
|
||||
// })).run()) {
|
||||
|
||||
// } else {
|
||||
// print.info('😔 ok, install it and run ..');
|
||||
// await toolbox.menu.showMenu();
|
||||
// }
|
||||
// }
|
||||
|
||||
system.run('touch ~/.pwcli_settings');
|
||||
print.info(`Created configuration file ${chalk.yellow(`~/.pwcli_settings`)}`);
|
||||
const config = {
|
||||
gh_token: '',
|
||||
photowall_repo: '',
|
||||
};
|
||||
|
||||
config.gh_token = await (new Input({
|
||||
message: `A github accesstoken is required with ${chalk.yellow('Full control of private repositories')}, please insert the key >`
|
||||
})).run();
|
||||
|
||||
config.photowall_repo = await (new Input({
|
||||
message: `If you want to be able to push active branch in your photowall repository, please insert the absolute path to the project >`
|
||||
})).run();
|
||||
|
||||
print.info(`
|
||||
|
||||
${JSON.stringify(config)}
|
||||
|
||||
`);
|
||||
if (await (new Confirm({
|
||||
message: `Ok to save the these settings to ${chalk.yellow(`~/.pwcli_settings`)}?`
|
||||
})).run()) {
|
||||
system.run(`echo '${JSON.stringify(config)}' > ~/.pwcli_settings`);
|
||||
} else {
|
||||
print.info('Ok then.');
|
||||
if (toolbox.fromMenu()) await toolbox.menu.showMenu();
|
||||
}
|
||||
|
||||
if (toolbox.fromMenu()) await toolbox.menu.showMenu();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu';
|
||||
|
||||
/**
|
||||
* Section1 menu
|
||||
*/
|
||||
module.exports = {
|
||||
name: 'setup',
|
||||
alias: ['s'],
|
||||
description: 'Setup pwcli (s)',
|
||||
hidden: true,
|
||||
run: async (toolbox: GluegunMenuToolbox) => {
|
||||
await toolbox.menu.showMenu('setup');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
import { GluegunToolbox } from 'gluegun'
|
||||
|
||||
|
||||
// add your CLI-specific functionality here, which will then be accessible
|
||||
// to your commands
|
||||
module.exports = (toolbox: GluegunToolbox) => {
|
||||
toolbox.foo = () => {
|
||||
toolbox.print.info('called foo extension')
|
||||
}
|
||||
|
||||
// enable this if you want to read configuration in from
|
||||
// the current folder's package.json (in a "pwcli" property),
|
||||
// pwcli.config.json, etc.
|
||||
// toolbox.config = {
|
||||
// ...toolbox.config,
|
||||
// ...toolbox.config.loadConfig("pwcli", process.cwd())
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// export types
|
||||
Reference in New Issue
Block a user