fix create/delete when there exists broken stacks

This commit is contained in:
Anders Gustafsson
2023-04-12 15:44:51 +02:00
parent 87bbc14dad
commit feb8342bc1
5 changed files with 109 additions and 225 deletions
@@ -1,18 +1,11 @@
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu'; import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu';
import { exit } from 'process'; import { exit } from 'process';
import { getTestStacksInfo } from '../../../../common/testservers';
const chalk = require('chalk'); const chalk = require('chalk');
const { AutoComplete, Input, Confirm } = require('enquirer'); const { AutoComplete, Input, Confirm } = require('enquirer');
import { defaultMenuSettings, getSettings } from '../../../../globals'; import { defaultMenuSettings, getSettings } from '../../../../globals';
import { getRepoBranches } from '../../../../services/github_rest'; import { getRepoBranches } from '../../../../services/github_rest';
type Stack = {
name: string;
status: string;
drift_status: string;
branch: string;
updated: string;
};
const getConfirmation = async (branchName: string, stackName: string) => { const getConfirmation = async (branchName: string, stackName: string) => {
const url = stackName.replace('photowall-', ''); const url = stackName.replace('photowall-', '');
const confirm = new Confirm({ const confirm = new Confirm({
@@ -36,7 +29,7 @@ module.exports = {
description: 'Create new testserver on aws (c)', description: 'Create new testserver on aws (c)',
hidden: false, hidden: false,
run: async (toolbox: GluegunMenuToolbox) => { run: async (toolbox: GluegunMenuToolbox) => {
const { system, strings, print, filesystem } = toolbox; const { system, strings, print } = toolbox;
// Load settings // Load settings
const config = await getSettings(toolbox); const config = await getSettings(toolbox);
@@ -67,47 +60,9 @@ module.exports = {
}); });
const branchName = await promptBranch.run(); const branchName = await promptBranch.run();
const stacks = JSON.parse( const testStacks = await getTestStacksInfo(toolbox);
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 = testStacks.map(item => {
const options = curratedStacks.map(item => {
return item.name; return item.name;
}); });
@@ -1,17 +1,10 @@
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu'; import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu';
import { GluegunSystem } from 'gluegun'; import { GluegunSystem } from 'gluegun';
import { defaultMenuSettings } from '../../../../globals'; import { defaultMenuSettings } from '../../../../globals';
import { getTestStacksInfo } from '../../../../common/testservers';
const chalk = require('chalk'); const chalk = require('chalk');
const { AutoComplete, Input, Confirm } = require('enquirer'); 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 getConfirmation = async (stackName: string) => {
const confirm = new Confirm({ const confirm = new Confirm({
name: 'confirm', name: 'confirm',
@@ -57,49 +50,11 @@ module.exports = {
description: 'Choose a testserver to delete (d)', description: 'Choose a testserver to delete (d)',
hidden: false, hidden: false,
run: async (toolbox: GluegunMenuToolbox) => { run: async (toolbox: GluegunMenuToolbox) => {
const { system, strings, print } = toolbox; const { system, strings } = toolbox;
const stacks = JSON.parse( const testStacks = await getTestStacksInfo(toolbox);
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 = testStacks.map(item => {
const options = curratedStacks.map(item => {
return item.name; return item.name;
}); });
@@ -116,6 +71,9 @@ module.exports = {
const shouldRun = await getConfirmation(stackName); const shouldRun = await getConfirmation(stackName);
if (shouldRun) { if (shouldRun) {
// Order here matters. Try to delete things in the order
// they are created on stack creation so we can delete everything that created
// during a failed create stack operation.
await runCommand( await runCommand(
system, system,
await confirmCommand( await confirmCommand(
@@ -128,18 +86,6 @@ module.exports = {
`aws ecr delete-repository --force --repository-name ${stackName}` `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 const artifacts = strings
.trim( .trim(
await system.run( await system.run(
@@ -153,6 +99,18 @@ module.exports = {
await confirmCommand(`aws s3 rb --force s3://${artifact}`) await confirmCommand(`aws s3 rb --force s3://${artifact}`)
); );
} }
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}`
)
);
} }
if (toolbox.fromMenu()) { if (toolbox.fromMenu()) {
@@ -1,82 +1,16 @@
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu'; import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu';
import chalk = require('chalk'); import chalk = require('chalk');
import { defaultMenuSettings } from '../../../../globals'; import { defaultMenuSettings } from '../../../../globals';
import { getTestStacksInfo } from '../../../../common/testservers';
const tto = require('terminal-table-output').create(); const tto = require('terminal-table-output').create();
type Stack = {
name: string;
status: string;
drift_status: string;
branch: string;
updated: string;
created: string;
};
module.exports = { module.exports = {
name: 'status', name: 'status',
alias: ['s'], alias: ['s'],
description: 'Show status for available testservers (s)', description: 'Show status for available testservers (s)',
hidden: false, hidden: false,
run: async (toolbox: GluegunMenuToolbox) => { run: async (toolbox: GluegunMenuToolbox) => {
const { system, strings } = toolbox; const testStacks = await getTestStacksInfo(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',
'ROLLBACK_COMPLETE',
].includes(item.StackStatus)
) {
try {
const pipeline = JSON.parse(
strings.trim(
await system.run(
`aws codepipeline get-pipeline --name ${item.StackName}Pipeline`
)
)
);
const sourceStage = pipeline.pipeline.stages.filter(stage => {
return stage.name === 'Source';
});
const branch =
sourceStage[0].actions[0].configuration.Branch ??
sourceStage[0].actions[0].configuration.BranchName;
const lastUpdated = pipeline.metadata.updated;
return {
name: item.StackName,
status: item.StackStatus,
drift_status: item.DriftInformation.StackDriftStatus,
branch: branch,
updated: lastUpdated,
created: item.CreationTime,
};
} catch (e) {
return {
name: item.StackName,
status: item.StackStatus,
drift_status: '',
branch: '',
updated: '',
created: item.CreationTime,
};
}
}
return Promise.resolve(null);
})
);
const curratedStacks: Stack[] = teststacks.filter(Boolean);
const status = (status: string) => { const status = (status: string) => {
switch (status) { switch (status) {
case 'UPDATE_FAILED': case 'UPDATE_FAILED':
@@ -88,7 +22,7 @@ module.exports = {
return `(${chalk.yellow('IN PROGRESS')})`; return `(${chalk.yellow('IN PROGRESS')})`;
case 'UPDATE_COMPLETE': case 'UPDATE_COMPLETE':
case 'CREATE_COMPLETE': case 'CREATE_COMPLETE':
return `(${chalk.greenBright('OK')})`; return `(${chalk.green('OK')})`;
default: default:
return `(${chalk.red.bold('?')})`; return `(${chalk.red.bold('?')})`;
} }
@@ -142,18 +76,20 @@ module.exports = {
.pushrow(['| status', 'stack', 'branch', 'time since update', 'url']) .pushrow(['| status', 'stack', 'branch', 'time since update', 'url'])
.line('-'); .line('-');
// Sort on stackname // Sort on stackname
curratedStacks.sort((a, b) => { testStacks.sort((a, b) => {
if (a.name < b.name) return -1; if (a.name < b.name) return -1;
if (a.name > b.name) return 1; if (a.name > b.name) return 1;
return 0; return 0;
}); });
for (const stack of curratedStacks) { for (const stack of testStacks) {
const t = new Date(stack.updated); const t = new Date(stack.updated);
tto.pushrow([ tto.pushrow([
status(stack.status), status(stack.status),
stack.name, stack.name,
ellipsis(stack.branch, 24), ellipsis(stack.branch, 24),
!isNaN(t.getTime()) ? timeSince(t.getTime()) : timeSince((new Date(stack.created)).getTime()), !isNaN(t.getTime())
? timeSince(t.getTime())
: timeSince(new Date(stack.created).getTime()),
stackUrl(stack.name) stackUrl(stack.name)
]); ]);
} }
@@ -3,6 +3,7 @@ const chalk = require('chalk');
const { AutoComplete, Input, Confirm } = require('enquirer'); const { AutoComplete, Input, Confirm } = require('enquirer');
import { Octokit } from '@octokit/core'; import { Octokit } from '@octokit/core';
import { exit } from 'process'; import { exit } from 'process';
import { getTestStacksInfo } from '../../../../common/testservers';
import { import {
defaultMenuSettings, defaultMenuSettings,
getCurrentBranch, getCurrentBranch,
@@ -99,47 +100,8 @@ module.exports = {
); );
} }
const stacks = JSON.parse( const testStacks = await getTestStacksInfo(toolbox);
strings.trim(await system.run(`aws cloudformation list-stacks`)) const options = testStacks.map(item => {
);
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; return item.name;
}); });
+73
View File
@@ -0,0 +1,73 @@
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu';
export type Stack = {
name: string;
status: string;
drift_status: string;
branch: string;
updated: string;
created: string;
};
export const getTestStacksInfo = async (
toolbox: GluegunMenuToolbox
): Promise<Stack[]> => {
const { strings, system } = toolbox;
const stacks = JSON.parse(
strings.trim(await system.run(`aws cloudformation list-stacks`))
);
const allStacks = 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',
'ROLLBACK_COMPLETE'
].includes(item.StackStatus)
) {
try {
const pipeline = JSON.parse(
strings.trim(
await system.run(
`aws codepipeline get-pipeline --name ${item.StackName}Pipeline`
)
)
);
const sourceStage = pipeline.pipeline.stages.filter(stage => {
return stage.name === 'Source';
});
const branch =
sourceStage[0].actions[0].configuration.Branch ??
sourceStage[0].actions[0].configuration.BranchName;
const lastUpdated = pipeline.metadata.updated;
return {
name: item.StackName,
status: item.StackStatus,
drift_status: item.DriftInformation.StackDriftStatus,
branch: branch,
updated: lastUpdated,
created: item.CreationTime
};
} catch (e) {
return {
name: item.StackName,
status: item.StackStatus,
drift_status: '',
branch: '',
updated: '',
created: item.CreationTime
};
}
}
return Promise.resolve(null);
})
) as Stack[] | null;
return allStacks.filter(Boolean);
};