From feb8342bc1a6f62b97b0bc77397ef4dd6d298b26 Mon Sep 17 00:00:00 2001 From: Anders Gustafsson Date: Wed, 12 Apr 2023 15:44:51 +0200 Subject: [PATCH] fix create/delete when there exists broken stacks --- .../deployment/testservers/create/create.ts | 57 ++----------- .../deployment/testservers/delete/delete.ts | 80 +++++-------------- .../deployment/testservers/status/status.ts | 80 ++----------------- .../deployment/testservers/update/update.ts | 44 +--------- src/common/testservers.ts | 73 +++++++++++++++++ 5 files changed, 109 insertions(+), 225 deletions(-) create mode 100644 src/common/testservers.ts diff --git a/src/commands/deployment/testservers/create/create.ts b/src/commands/deployment/testservers/create/create.ts index 368da21..0f97510 100644 --- a/src/commands/deployment/testservers/create/create.ts +++ b/src/commands/deployment/testservers/create/create.ts @@ -1,18 +1,11 @@ import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu'; import { exit } from 'process'; +import { getTestStacksInfo } from '../../../../common/testservers'; const chalk = require('chalk'); const { AutoComplete, Input, Confirm } = require('enquirer'); import { defaultMenuSettings, getSettings } from '../../../../globals'; 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 url = stackName.replace('photowall-', ''); const confirm = new Confirm({ @@ -36,7 +29,7 @@ module.exports = { description: 'Create new testserver on aws (c)', hidden: false, run: async (toolbox: GluegunMenuToolbox) => { - const { system, strings, print, filesystem } = toolbox; + const { system, strings, print } = toolbox; // Load settings const config = await getSettings(toolbox); @@ -67,47 +60,9 @@ module.exports = { }); const branchName = await promptBranch.run(); - 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 testStacks = await getTestStacksInfo(toolbox); - const curratedStacks: Stack[] = teststacks.filter(Boolean); - const options = curratedStacks.map(item => { + const options = testStacks.map(item => { return item.name; }); @@ -121,11 +76,11 @@ module.exports = { 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'); diff --git a/src/commands/deployment/testservers/delete/delete.ts b/src/commands/deployment/testservers/delete/delete.ts index ced927e..43596ac 100644 --- a/src/commands/deployment/testservers/delete/delete.ts +++ b/src/commands/deployment/testservers/delete/delete.ts @@ -1,17 +1,10 @@ import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu'; import { GluegunSystem } from 'gluegun'; import { defaultMenuSettings } from '../../../../globals'; +import { getTestStacksInfo } from '../../../../common/testservers'; 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', @@ -57,49 +50,11 @@ module.exports = { description: 'Choose a testserver to delete (d)', hidden: false, run: async (toolbox: GluegunMenuToolbox) => { - const { system, strings, print } = toolbox; + 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 testStacks = await getTestStacksInfo(toolbox); - const curratedStacks: Stack[] = teststacks.filter(Boolean); - const options = curratedStacks.map(item => { + const options = testStacks.map(item => { return item.name; }); @@ -116,6 +71,9 @@ module.exports = { const shouldRun = await getConfirmation(stackName); 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( system, await confirmCommand( @@ -128,18 +86,6 @@ module.exports = { `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( @@ -153,6 +99,18 @@ module.exports = { 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()) { diff --git a/src/commands/deployment/testservers/status/status.ts b/src/commands/deployment/testservers/status/status.ts index bd2bcf9..ba1dfc0 100644 --- a/src/commands/deployment/testservers/status/status.ts +++ b/src/commands/deployment/testservers/status/status.ts @@ -1,82 +1,16 @@ import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu'; import chalk = require('chalk'); import { defaultMenuSettings } from '../../../../globals'; +import { getTestStacksInfo } from '../../../../common/testservers'; const tto = require('terminal-table-output').create(); -type Stack = { - name: string; - status: string; - drift_status: string; - branch: string; - updated: string; - created: string; -}; - module.exports = { name: 'status', alias: ['s'], description: 'Show status for available testservers (s)', hidden: false, 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', - '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 testStacks = await getTestStacksInfo(toolbox); const status = (status: string) => { switch (status) { case 'UPDATE_FAILED': @@ -88,7 +22,7 @@ module.exports = { return `(${chalk.yellow('IN PROGRESS')})`; case 'UPDATE_COMPLETE': case 'CREATE_COMPLETE': - return `(${chalk.greenBright('OK')})`; + return `(${chalk.green('OK')})`; default: return `(${chalk.red.bold('?')})`; } @@ -142,18 +76,20 @@ module.exports = { .pushrow(['| status', 'stack', 'branch', 'time since update', 'url']) .line('-'); // 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; return 0; }); - for (const stack of curratedStacks) { + for (const stack of testStacks) { const t = new Date(stack.updated); tto.pushrow([ status(stack.status), stack.name, 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) ]); } diff --git a/src/commands/deployment/testservers/update/update.ts b/src/commands/deployment/testservers/update/update.ts index d7cacb2..9207f75 100644 --- a/src/commands/deployment/testservers/update/update.ts +++ b/src/commands/deployment/testservers/update/update.ts @@ -3,6 +3,7 @@ const chalk = require('chalk'); const { AutoComplete, Input, Confirm } = require('enquirer'); import { Octokit } from '@octokit/core'; import { exit } from 'process'; +import { getTestStacksInfo } from '../../../../common/testservers'; import { defaultMenuSettings, getCurrentBranch, @@ -99,47 +100,8 @@ module.exports = { ); } - 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 => { + const testStacks = await getTestStacksInfo(toolbox); + const options = testStacks.map(item => { return item.name; }); diff --git a/src/common/testservers.ts b/src/common/testservers.ts new file mode 100644 index 0000000..71a8f4e --- /dev/null +++ b/src/common/testservers.ts @@ -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 => { + 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); +};