Skip to content
This repository has been archived by the owner on May 18, 2023. It is now read-only.

Commit

Permalink
Extracting nested stack error (#35)
Browse files Browse the repository at this point in the history
* Extracting nested stack error into the terminal console in case of failure deployment

* improved console logging for errors
  • Loading branch information
zimny authored Jul 16, 2020
1 parent 2132c44 commit 25f4423
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 21 deletions.
23 changes: 22 additions & 1 deletion __mocks__/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,28 @@ const configMock: IConfig = {
has (setting: string): boolean {
return setting in config
},
util: null as any
util: {
loadFileConfigs (): any {
return {
app: {
name: 'S3Webhosting',
prefix: 'Nf'
},
dev: {
target: 'default'
},
accounts: {
default: {
env: {
account: '101259067028',
region: 'eu-west-1'
},
profile: 'mira-dev'
}
}
}
}
} as any
}

export default configMock
1 change: 1 addition & 0 deletions docs/overview/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@ __Note:__
- The web app and API use HTTPS
- DB data is encrypted at rest and in transit
- S3 buckets used for CICD pipeline are encrypted

14 changes: 14 additions & 0 deletions docs/quick-start/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,20 @@ Keep in mind, AWS CDK requires all the modules to be the same version.
}
```
## Troubleshooting
In case your application fails to deployed, make sure that your config it properly structured and your stack definition is correct.
Mira extracts the errors from the failed nested stacks to your terminal window, so it should help you to quickly find the root cause.
Typical issues includes:
* Version mismatch for the AWS CDK, between Mira and your local package.json.
* A `profile` property is not defined for all environments in the config file.
* The code you're trying to deploy was not complied with TypeScript after recent changes.



<!---- External links ---->
[docs]: https://nf-mira.netlify.com/?#/
[nvm]: https://github.com/nvm-sh/nvm
Expand Down
6 changes: 5 additions & 1 deletion src/cdk/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MiraServiceStack, MiraStack } from './stack'
import { Stack } from '@aws-cdk/core'
import * as fs from 'fs'
import * as path from 'path'
import { getBaseStackName } from './constructs/config/utils'
import { getBaseStackName, getBaseStackNameFromParams } from './constructs/config/utils'
import { MiraConfig } from '../config/mira-config'
// eslint-disable-next-line
const minimist = require('minimist')
Expand Down Expand Up @@ -81,6 +81,10 @@ export class MiraApp {
return getBaseStackName(suffix)
}

static getBaseStackNameFromParams (prefix: string, name: string, suffix?: string): string {
return getBaseStackNameFromParams(prefix, name, suffix)
}

/**
* Initializes the app and stack.
*/
Expand Down
132 changes: 127 additions & 5 deletions src/cdk/bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import config from 'config'
import _ from 'lodash'
import mockConfig from './../config/__mocks__/default.json'
import yargs from 'yargs'
import { MiraConfig } from '../config/mira-config'
const assumeRoleMock = jest.fn()

jest.mock('../assume-role', () => ({
Expand All @@ -12,6 +13,8 @@ const MiraBootstrap = require('./bootstrap').MiraBootstrap

jest.mock('config')

MiraConfig.getEnvironment = jest.fn()

const mockConfigHandler = (mockConfig: any): void => {
config.get = (key: string): any => _.get(mockConfig, key)
config.has = (key: string): any => _.has(mockConfig, key)
Expand All @@ -34,10 +37,13 @@ describe('MiraBootstrap', () => {

describe('MiraBootstrap deploy', () => {
const miraBootstrapInstance = new MiraBootstrap()
miraBootstrapInstance.spawn = jest.fn()
it('throws error for missing dev.target', async () => {
await expect(miraBootstrapInstance.deploy()).rejects.toThrow('Missing dev.target in your config file.')
})
miraBootstrapInstance.spawn = () => {
return {
on: (code: string, fn: Function) => {
fn(0)
}
}
}
it('do not calls assume role when no role provided in the cli', async () => {
mockConfigHandler(mockConfig)
miraBootstrapInstance.args = yargs(['']).argv
Expand All @@ -55,7 +61,11 @@ describe('MiraBootstrap deploy', () => {
it('spawns new process with the right parameters', async () => {
mockConfigHandler(mockConfig)
miraBootstrapInstance.args = yargs().argv
miraBootstrapInstance.spawn.mockClear()
miraBootstrapInstance.spawn = jest.fn().mockReturnValue({
on: (code: string, fn: Function) => {
fn(0)
}
})
await miraBootstrapInstance.deploy()
expect(miraBootstrapInstance.spawn.mock.calls[0][0]).toBe('node')
const cdkExecutablePath = miraBootstrapInstance.spawn.mock.calls[0][1][0]
Expand All @@ -66,3 +76,115 @@ describe('MiraBootstrap deploy', () => {
expect(miraBootstrapInstance.spawn.mock.calls[0][1][5]).toBe('--profile=mira-dev')
})
})

describe('MiraBootstrap getFirstFailedNestedStackName', () => {
const miraBootstrapInstance = new MiraBootstrap()
MiraConfig.getEnvironment = jest.fn().mockReturnValue({
env: {
account: '101259067028',
region: 'eu-west-1'
},
profile: 'mira-dev',
name: 'default'
})
it('gets first CREATE_FAILED NestedStack', async () => {
miraBootstrapInstance.getAwsSdkConstruct = jest.fn().mockReturnValue({
describeStackEvents: () => {
return {
promise: () => {
return Promise.resolve({
StackEvents: [
{
PhysicalResourceId: 'arn:aws:cloudformation:eu-west-1:101259067028:stack/Nf-S3Webhosting-Service-default-S3Webhosting0NestedStackS3Webhosting0NestedStackR-OSY009YIBDSG/b2e7f2d0-bd20-11ea-86e0-0a2c3f6a2a32',
ResourceStatus: 'CREATE_FAILED'
}]
})
}
}
}
})
const rsp = await miraBootstrapInstance.getFirstFailedNestedStackName()
expect(rsp).toEqual('arn:aws:cloudformation:eu-west-1:101259067028:stack/Nf-S3Webhosting-Service-default-S3Webhosting0NestedStackS3Webhosting0NestedStackR-OSY009YIBDSG/b2e7f2d0-bd20-11ea-86e0-0a2c3f6a2a32')
})
it('gets first UPDATE_FAILED NestedStack', async () => {
miraBootstrapInstance.getAwsSdkConstruct = jest.fn().mockReturnValue({
describeStackEvents: () => {
return {
promise: () => {
return Promise.resolve({
StackEvents: [
{
PhysicalResourceId: 'arn:aws:cloudformation:eu-west-1:101259067028:stack/Nf-S3Webhosting-Service-default-S3Webhosting0NestedStackS3Webhosting0NestedStackR-OSY009YIBDSG/b2e7f2d0-bd20-11ea-86e0-0a2c3f6a2a32',
ResourceStatus: 'UPDATE_FAILED'
}]
})
}
}
}
})
const rsp = await miraBootstrapInstance.getFirstFailedNestedStackName()
expect(rsp).toEqual('arn:aws:cloudformation:eu-west-1:101259067028:stack/Nf-S3Webhosting-Service-default-S3Webhosting0NestedStackS3Webhosting0NestedStackR-OSY009YIBDSG/b2e7f2d0-bd20-11ea-86e0-0a2c3f6a2a32')
})

it('gets no NestedStack if ResourceStatus positive', async () => {
miraBootstrapInstance.getAwsSdkConstruct = jest.fn().mockReturnValue({
describeStackEvents: () => {
return {
promise: () => {
return Promise.resolve({
StackEvents: [
{
PhysicalResourceId: 'arn:aws:cloudformation:eu-west-1:101259067028:stack/Nf-S3Webhosting-Service-default-S3Webhosting0NestedStackS3Webhosting0NestedStackR-OSY009YIBDSG/b2e7f2d0-bd20-11ea-86e0-0a2c3f6a2a32',
ResourceStatus: 'UPDATE_IN_PROGRESS'
}]
})
}
}
}
})
const rsp = await miraBootstrapInstance.getFirstFailedNestedStackName()
expect(rsp).toBeUndefined()
})
})

describe('MiraBootstrap extractNestedStackError', () => {
const miraBootstrapInstance = new MiraBootstrap()
MiraConfig.getEnvironment = jest.fn().mockReturnValue({
env: {
account: '101259067028',
region: 'eu-west-1'
},
profile: 'mira-dev',
name: 'default'
})
const failedResourceCreation = {
StackId: 'arn:aws:cloudformation:eu-west-1:101259067028:stack/Nf-S3Webhosting-Service-default-S3Webhosting0NestedStackS3Webhosting0NestedStackR-OSY009YIBDSG/b2e7f2d0-bd20-11ea-86e0-0a2c3f6a2a32',
EventId: 'SiteBucket21DC2FA83-CREATE_FAILED-2020-07-10T10:02:37.320Z',
StackName: 'Nf-S3Webhosting-Service-default-S3Webhosting0NestedStackS3Webhosting0NestedStackR-OSY009YIBDSG',
LogicalResourceId: 'SiteBucket21DC2FA83',
PhysicalResourceId: 'arn:aws:cloudformation:eu-west-1:101259067028:stack/Nf-S3Webhosting-Service-default-S3Webhosting0NestedStackS3Webhosting0NestedStackR-OSY009YIBDSG/b2e7f2d0-bd20-11ea-86e0-0a2c3f6a2a32',
ResourceType: 'AWS::S3::Bucket',
Timestamp: '2020-07-10T10:02:37.320Z',
ResourceStatus: 'CREATE_FAILED',
ResourceStatusReason: 'Resource creation cancelled',
ResourceProperties: '{"BucketName":"someName"}'
}
miraBootstrapInstance.getAwsSdkConstruct = jest.fn().mockReturnValue({
describeStackEvents: () => {
return {
promise: () => {
return Promise.resolve({
StackEvents: [
failedResourceCreation
]
})
}
}
}
})
miraBootstrapInstance.getFirstFailedNestedStackName = jest.fn().mockReturnValue('some_stack_name')
it('gets error from the nested stack if deploy is failed', async () => {
const rsp = await miraBootstrapInstance.extractNestedStackError()
expect(rsp).toEqual([failedResourceCreation])
})
})
98 changes: 84 additions & 14 deletions src/cdk/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import configWizard from './constructs/config/make-default-config'
import { assumeRole } from '../assume-role'
import yargs from 'yargs'
import Transpiler from '../transpiler'
import configModule from 'config'
import aws from 'aws-sdk'
import { StackEvent } from 'aws-sdk/clients/cloudformation'

/**
* @class Responsible for beaming up bits to AWS. Teleportation device not
* included.
Expand Down Expand Up @@ -70,20 +74,23 @@ export class MiraBootstrap {
this.env ? `--profile=${this.getProfile(this.env)}` : '',
...additionalArgs
]
try {
this.spawn(
'node',
commandOptions, {
stdio: 'inherit',
env: {
NODE_ENV: 'dev',
...process.env
}
})
} catch (error) {
console.error(error.message)
process.exit(1)
}
const proc = this.spawn(
'node',
commandOptions, {
stdio: 'inherit',
env: {
NODE_ENV: 'dev',
...process.env
}
})
await new Promise((resolve) => {
proc.on('exit', async (code) => {
if (code !== 0) {
await this.printExtractedNestedStackErrors()
}
resolve()
})
})
}

/**
Expand Down Expand Up @@ -370,4 +377,67 @@ export class MiraBootstrap {
async undeploy (): Promise<void> {
return await this.deploy(true)
}

/**
* Changes context to use dev config if available, and runs passed function.
* Typical usecase for this function is to set Dev environment in the context of Mira executable.
* In case of CDK executions 'dev' is set as NODE_ENV during spawn.
* @param fn
* @param params
*/
useDevConfig (fn: Function, params: any): any {
const tmpEnv = process.env.NODE_ENV || 'default'
process.env.NODE_ENV = 'dev'
const rsp = fn.apply(this, params)
process.env.NODE_ENV = tmpEnv
return rsp
}

getServiceStackName (account: Account): string {
const tmpConfig = configModule.util.loadFileConfigs(path.join(process.cwd(), 'config'))
return `${MiraApp.getBaseStackNameFromParams(tmpConfig.app.prefix, tmpConfig.app.name, 'Service')}-${account.name}`
}

getAwsSdkConstruct (construct: any, account: Account): any {
const credentials = new aws.SharedIniFileCredentials({ profile: this.getProfile(this.env) || '' })
aws.config.credentials = credentials
// eslint-disable-next-line
// @ts-ignore
return new aws[construct]({ region: account.env.region })
}

async getFirstFailedNestedStackName (account: Account, stackName: string): Promise<string> {
const cloudformation = this.getAwsSdkConstruct('CloudFormation', account)
const events = await cloudformation.describeStackEvents({ StackName: stackName }).promise()
return events.StackEvents?.filter((event: StackEvent) => event.ResourceStatus === 'UPDATE_FAILED' || event.ResourceStatus === 'CREATE_FAILED')[0]?.PhysicalResourceId
}

async extractNestedStackError () {
const account: Account = MiraConfig.getEnvironment(this.env)
const stackName = this.useDevConfig(this.getServiceStackName, [account])
// Environment variable required to parse ~/.aws/config file with profiles.
process.env.AWS_SDK_LOAD_CONFIG = '1'

let events
try {
const nestedStackName = await this.getFirstFailedNestedStackName(account, stackName)
const cloudformation = this.getAwsSdkConstruct('CloudFormation', account)
events = await cloudformation.describeStackEvents({ StackName: nestedStackName }).promise()
} catch (e) {
console.log(chalk.red('Error, while getting error message from cloudformation. Seems something is wrong with your configuration.'))
}
return events?.StackEvents?.filter((event: StackEvent) => event.ResourceStatus === 'UPDATE_FAILED' || event.ResourceStatus === 'CREATE_FAILED')
}

async printExtractedNestedStackErrors (): Promise<any> {
const printCarets = (nb: number) => {
return '^'.repeat(nb)
}
const failedResources = await this.extractNestedStackError()
console.log(chalk.red('\n\nYour app failed deploying, one of your nested stacks have failed to create or update resources. See the list of failed resources below:'))
failedResources.forEach((item: any) => {
console.log(chalk.red(`\n* ${item.ResourceStatus} - ${item.LogicalResourceId}\nReason: ${item.ResourceStatusReason}\nTime: ${item.Timestamp}\n`))
})
console.log(chalk.red(`\n\n${printCarets(100)}\nAnalyze the list above, to find why your stack failed deployment.`))
}
}
11 changes: 11 additions & 0 deletions src/cdk/constructs/config/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ export function getBaseStackName (suffix? : string): string {
.map((p) => pascalCase(p as string))
return output.join('-')
}
export function getBaseStackNameFromParams (prefix: string, name: string, suffix? : string): string {
const pieces = [
prefix,
name,
suffix
]
const output = pieces
.filter((p) => p)
.map((p) => pascalCase(p as string))
return output.join('-')
}

/**
* @deprecated
Expand Down

0 comments on commit 25f4423

Please sign in to comment.