Skip to content

Commit

Permalink
SAM debug: detect & surface "low disk space" #171
Browse files Browse the repository at this point in the history
If `sam build` fills up the disk while building a Docker image
(--use-container) it may fail with this message:
    RuntimeError: Container does not exist
  • Loading branch information
justinmk3 authored Nov 23, 2020
1 parent effb58a commit 44dad84
Show file tree
Hide file tree
Showing 9 changed files with 61 additions and 16 deletions.
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@
"AWS.s3.validateBucketName.error.invalidStart": "Bucket name must start with a lowercase letter or number",
"AWS.s3.validateBucketName.error.misusedPeriods": "Periods in bucket name must be surrounded by a lowercase letter or number",
"AWS.s3.validateBucketName.error.resemblesIpAddress": "Bucket name must not resemble an IP address",
"AWS.sam.build.failure.diskSpace": "\"sam build\" failed. Check system disk space.",
"AWS.sam.debugger.invalidRequest": "Debug Configuration has an unsupported request type. Supported types: {0}",
"AWS.sam.debugger.invalidTarget": "Debug Configuration has an unsupported target type. Supported types: {0}",
"AWS.sam.debugger.invalidRuntime": "AWS SAM debug: unknown runtime: {0}",
Expand Down
28 changes: 25 additions & 3 deletions src/shared/sam/cli/samCliBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { logAndThrowIfUnexpectedExitCode, SamCliProcessInvoker } from './samCliI
import { DefaultSamCliProcessInvoker } from './samCliInvoker'
import { pushIf } from '../../utilities/collectionUtils'
import { ext } from '../../extensionGlobals'
import { getChannelLogger } from '../../utilities/vsCodeUtils'
import { getChannelLogger, localize } from '../../utilities/vsCodeUtils'

export interface SamCliBuildInvocationArguments {
/**
Expand Down Expand Up @@ -70,6 +70,8 @@ export interface FileFunctions {
* An elaborate way to run `sam build`.
*/
export class SamCliBuildInvocation {
private _failure: string | undefined

public constructor(
private readonly args: SamCliBuildInvocationArguments,
private readonly context: { file: FileFunctions } = { file: getDefaultFileFunctions() }
Expand All @@ -79,9 +81,15 @@ export class SamCliBuildInvocation {
this.args.skipPullImage = !!this.args.skipPullImage
}

/** Gets the failure message, or undefined if no failure was detected. */
public failure(): string | undefined {
return this._failure
}

/**
* Invokes "sam build".
*
* @returns process exit/status code
* @returns Process exit code, or -1 if `SamCliBuildInvocation` stopped the process and stored a failure message in `SamCliBuildInvocation.failure()`.
*/
public async execute(): Promise<number> {
await this.validate()
Expand Down Expand Up @@ -113,14 +121,28 @@ export class SamCliBuildInvocation {
...this.args.environmentVariables,
}

const checkFailure = (text: string): void => {
if (text.match(/(RuntimeError: Container does not exist)/)) {
this.args.invoker.stop()
this._failure = localize(
'AWS.sam.build.failure.diskSpace',
'"sam build" failed. Check system disk space.'
)
}
}

const childProcessResult = await this.args.invoker.invoke({
spawnOptions: { env },
arguments: invokeArgs,
channelLogger: getChannelLogger(ext.outputChannel),
onStdout: checkFailure,
onStderr: checkFailure,
})

if (this._failure) {
return -1
}
logAndThrowIfUnexpectedExitCode(childProcessResult, 0)

return childProcessResult.exitCode
}

Expand Down
26 changes: 18 additions & 8 deletions src/shared/sam/cli/samCliInvoker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,16 @@ export function resolveSamCliProcessInvokerContext(
* TODO: Merge this with `DefaultSamLocalInvokeCommand`.
*/
export class DefaultSamCliProcessInvoker implements SamCliProcessInvoker {
private childProcess?: ChildProcess
public constructor(private readonly context: SamCliProcessInvokerContext = resolveSamCliProcessInvokerContext()) {}

public stop(): void {
if (!this.childProcess) {
throw new Error('not started')
}
this.childProcess.stop()
}

public async invoke(options?: SamCliProcessInvokeOptions): Promise<ChildProcessResult> {
const invokeOptions = makeRequiredSamCliProcessInvokeOptions(options)
const logger = getLogger()
Expand All @@ -56,22 +64,24 @@ export class DefaultSamCliProcessInvoker implements SamCliProcessInvoker {
}

const samCommand = sam.path ? sam.path : 'sam'
const childProcess: ChildProcess = new ChildProcess(
samCommand,
invokeOptions.spawnOptions,
...invokeOptions.arguments
)
this.childProcess = new ChildProcess(samCommand, invokeOptions.spawnOptions, ...invokeOptions.arguments)

options?.channelLogger?.info('AWS.running.command', 'Running command: {0}', `${childProcess}`)
logger.verbose(`running: ${childProcess}`)
return await childProcess.run(
options?.channelLogger?.info('AWS.running.command', 'Running command: {0}', `${this.childProcess}`)
logger.verbose(`running: ${this.childProcess}`)
return await this.childProcess.run(
(text: string) => {
options?.channelLogger?.emitMessage(text)
logger.verbose(`stdout: ${text}`)
if (options?.onStdout) {
options.onStdout(text)
}
},
(text: string) => {
options?.channelLogger?.emitMessage(text)
logger.verbose(`stderr: ${text}`)
if (options?.onStderr) {
options.onStderr(text)
}
}
)
}
Expand Down
7 changes: 5 additions & 2 deletions src/shared/sam/cli/samCliInvokerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@

import { SpawnOptions } from 'child_process'
import { getLogger } from '../../logger'
import { ChildProcessResult } from '../../utilities/childProcess'
import { ChildProcessResult, ChildProcessStartArguments } from '../../utilities/childProcess'
import { ChannelLogger } from '../../utilities/vsCodeUtils'

export interface SamCliProcessInvokeOptions {
spawnOptions?: SpawnOptions
arguments?: string[]
/** Optionally log stdout and stderr to the specified logger */
channelLogger?: ChannelLogger
onStdout?: ChildProcessStartArguments['onStdout']
onStderr?: ChildProcessStartArguments['onStderr']
}

export function makeRequiredSamCliProcessInvokeOptions(
options?: SamCliProcessInvokeOptions
): Required<Omit<SamCliProcessInvokeOptions, 'channelLogger'>> {
): Required<Omit<SamCliProcessInvokeOptions, 'channelLogger' | 'onStdout' | 'onStderr'>> {
options = options || {}

return {
Expand All @@ -28,6 +30,7 @@ export function makeRequiredSamCliProcessInvokeOptions(

export interface SamCliProcessInvoker {
invoke(options?: SamCliProcessInvokeOptions): Promise<ChildProcessResult>
stop(): void
}

export function logAndThrowIfUnexpectedExitCode(processResult: ChildProcessResult, expectedExitCode: number): void {
Expand Down
7 changes: 6 additions & 1 deletion src/shared/sam/localLambdaRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,12 @@ export async function invokeLambdaFunction(
}

try {
await new SamCliBuildInvocation(samCliArgs).execute()
const samBuild = new SamCliBuildInvocation(samCliArgs)
await samBuild.execute()
if (samBuild.failure()) {
ctx.chanLogger.emitMessage(samBuild.failure()!)
throw new Error(samBuild.failure())
}
} finally {
// always delete temp template.
await unlink(config.templatePath)
Expand Down
4 changes: 2 additions & 2 deletions src/shared/utilities/childProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ export class ChildProcess {
* Calls `start()` with default listeners that resolve()/reject() on process end.
*/
public async run(
onStdout?: (text: string) => void,
onStderr?: (text: string) => void
onStdout?: ChildProcessStartArguments['onStdout'],
onStderr?: ChildProcessStartArguments['onStderr']
): Promise<ChildProcessResult> {
return await new Promise<ChildProcessResult>(async (resolve, reject) => {
await this.start({
Expand Down
1 change: 1 addition & 0 deletions src/test/shared/codelens/localLambdaRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ describe('localLambdaRunner', async () => {
templatePath: tempDir,
manifestPath: undefined, // not needed for testing
invoker: {
stop: () => {},
invoke: async (): Promise<ChildProcessResult> =>
isSuccessfulBuild ? successfulChildProcess : failedChildProcess,
},
Expand Down
2 changes: 2 additions & 0 deletions src/test/shared/sam/cli/samCliTestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { ChildProcessResult } from '../../../../shared/utilities/childProcess'
export class MockSamCliProcessInvoker implements SamCliProcessInvoker {
public constructor(private readonly validateArgs: (args: string[]) => void) {}

public stop(): void {}

public async invoke(options?: SamCliProcessInvokeOptions): Promise<ChildProcessResult> {
const invokeSettings = makeRequiredSamCliProcessInvokeOptions(options)

Expand Down
1 change: 1 addition & 0 deletions src/test/shared/sam/cli/testSamCliProcessInvoker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { TestLogger } from '../../../testLogger'
export class TestSamCliProcessInvoker implements SamCliProcessInvoker {
public constructor(private readonly onInvoke: (spawnOptions: SpawnOptions, ...args: any[]) => ChildProcessResult) {}

public stop(): void {}
public async invoke(options?: SamCliProcessInvokeOptions): Promise<ChildProcessResult> {
const invokeOptions = makeRequiredSamCliProcessInvokeOptions(options)

Expand Down

0 comments on commit 44dad84

Please sign in to comment.